diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index decf0fd419..b2d1dcb3ee 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -28,7 +28,7 @@ import type { BrowserType } from '../../browserType'; export type Transport = { sendEvent?: (method: string, params: any) => void; - dispatch: (method: string, params: any) => Promise; + dispatch: (method: string, params: any) => Promise; close?: () => void; onclose: () => void; }; diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index 5451363abf..62ec66873c 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { openTraceViewerApp, openTraceInBrowser } from 'playwright-core/lib/server'; +import { openTraceViewerApp, openTraceInBrowser, registry } from 'playwright-core/lib/server'; import { isUnderTest, ManualPromise } from 'playwright-core/lib/utils'; import type { FullResult } from '../../reporter'; import { clearCompilationCache, collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache'; @@ -107,6 +107,13 @@ class UIMode { void this._stopTests(); return; } + if (method === 'checkBrowsers') + return { hasBrowsers: hasSomeBrowsers() }; + if (method === 'installBrowsers') { + await installBrowsers(); + return; + } + queue = queue.then(() => this._queueListOrRun(method, params)); await queue; }, @@ -297,3 +304,19 @@ class Watcher { this._collector.length = 0; } } + +function hasSomeBrowsers(): boolean { + for (const browserName of ['chromium', 'webkit', 'firefox']) { + try { + registry.findExecutable(browserName)!.executablePathOrDie('javascript'); + return true; + } catch { + } + } + return false; +} + +async function installBrowsers() { + const executables = registry.defaultExecutables(); + await registry.install(executables, false); +} diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index b0e61d31e4..6b63a2162c 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -84,6 +84,7 @@ export const UIModeView: React.FC<{}> = ({ const runTestBacklog = React.useRef>(new Set()); const [collapseAllCount, setCollapseAllCount] = React.useState(0); const [isDisconnected, setIsDisconnected] = React.useState(false); + const [hasBrowsers, setHasBrowsers] = React.useState(true); const inputRef = React.useRef(null); @@ -91,8 +92,10 @@ export const UIModeView: React.FC<{}> = ({ setIsLoading(true); setWatchedTreeIds({ value: new Set() }); updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), [], undefined); - refreshRootSuite(true).then(() => { + refreshRootSuite(true).then(async () => { setIsLoading(false); + const { hasBrowsers } = await sendMessage('checkBrowsers'); + setHasBrowsers(hasBrowsers); }); }, []); @@ -193,6 +196,14 @@ export const UIModeView: React.FC<{}> = ({ toggleTheme()} /> reloadTests()} disabled={isRunningTest || isLoading}> { setIsShowingOutput(!isShowingOutput); }} /> + {!hasBrowsers && { + setIsShowingOutput(true); + sendMessage('installBrowsers').then(async () => { + setIsShowingOutput(false); + const { hasBrowsers } = await sendMessage('checkBrowsers'); + setHasBrowsers(hasBrowsers); + }); + }} />} ({ autoExpandDepth, }: TreeViewProps) { const treeItems = React.useMemo(() => { - // Expand all ancestors of the selected item. - for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent) - treeState.expandedItems.set(item.id, true); - return flattenTree(rootItem, treeState.expandedItems, autoExpandDepth || 0); + return flattenTree(rootItem, selectedItem, treeState.expandedItems, autoExpandDepth || 0); }, [rootItem, selectedItem, treeState, autoExpandDepth]); // Filter visible items. @@ -160,11 +157,15 @@ type TreeItemData = { parent: TreeItem | null, }; -function flattenTree(rootItem: T, expandedItems: Map, autoExpandDepth: number): Map { +function flattenTree(rootItem: T, selectedItem: T | undefined, expandedItems: Map, autoExpandDepth: number): Map { const result = new Map(); + const temporaryExpanded = new Map(); + for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent) + temporaryExpanded.set(item.id, true); + const appendChildren = (parent: T, depth: number) => { for (const item of parent.children as T[]) { - const expandState = expandedItems.get(item.id); + const expandState = expandedItems.get(item.id) ?? temporaryExpanded.get(item.id); const autoExpandMatches = autoExpandDepth > depth && result.size < 25 && expandState !== false; const expanded = item.children.length ? expandState || autoExpandMatches : undefined; result.set(item, { depth, expanded, parent: rootItem === parent ? null : parent });