diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 02d834a758..57e1ca0912 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -28,6 +28,8 @@ import type { Page } from '../../page'; type Options = { app?: string, headless?: boolean, host?: string, port?: number, isServer?: boolean }; export async function showTraceViewer(traceUrls: string[], browserName: string, options?: Options): Promise { + const stdinServer = options?.isServer ? new StdinServer() : undefined; + const { headless = false, host, port, app } = options || {}; for (const traceUrl of traceUrls) { let traceFile = traceUrl; @@ -113,46 +115,59 @@ export async function showTraceViewer(traceUrls: string[], browserName: string, page.on('close', () => process.exit()); } + if (options?.isServer) + params.push('isServer'); const searchQuery = params.length ? '?' + params.join('&') : ''; await page.mainFrame().goto(serverSideCallMetadata(), urlPrefix + `/trace/${app || 'index.html'}${searchQuery}`); - - if (options?.isServer) - runServer(page); - + stdinServer?.setPage(page); return page; } -function runServer(page: Page) { - let liveTraceTimer: NodeJS.Timeout | undefined; - const loadTrace = (url: string) => { - clearTimeout(liveTraceTimer); - page.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`, false, undefined).catch(() => {}); - }; +class StdinServer { + private _pollTimer: NodeJS.Timeout | undefined; + private _traceUrl: string | undefined; + private _page: Page | undefined; - const pollLoadTrace = (url: string) => { - loadTrace(url); - liveTraceTimer = setTimeout(() => { - pollLoadTrace(url); + constructor() { + process.stdin.on('data', data => { + const url = data.toString().trim(); + if (url === this._traceUrl) + return; + this._traceUrl = url; + if (url.endsWith('.json')) + this._pollLoadTrace(url); + else + this._loadTrace(url); + }); + process.stdin.on('close', () => this._selfDestruct()); + } + + setPage(page: Page) { + this._page = page; + if (this._traceUrl) + this._loadTrace(this._traceUrl); + } + + private _loadTrace(url: string) { + clearTimeout(this._pollTimer); + this._page?.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`, false, undefined).catch(() => {}); + } + + private _pollLoadTrace(url: string) { + this._loadTrace(url); + this._pollTimer = setTimeout(() => { + this._pollLoadTrace(url); }, 500); - }; + } - process.stdin.on('data', data => { - const url = data.toString().trim(); - if (url.endsWith('.json')) - pollLoadTrace(url); - else - loadTrace(url); - }); - process.stdin.on('close', () => selfDestruct()); -} - -function selfDestruct() { - // Force exit after 30 seconds. - setTimeout(() => process.exit(0), 30000); - // Meanwhile, try to gracefully close all browsers. - gracefullyCloseAll().then(() => { - process.exit(0); - }); + private _selfDestruct() { + // Force exit after 30 seconds. + setTimeout(() => process.exit(0), 30000); + // Meanwhile, try to gracefully close all browsers. + gracefullyCloseAll().then(() => { + process.exit(0); + }); + } } function traceDescriptor(traceName: string) { diff --git a/packages/trace-viewer/src/sw.ts b/packages/trace-viewer/src/sw.ts index 79a8e2b7e3..6dec512f16 100644 --- a/packages/trace-viewer/src/sw.ts +++ b/packages/trace-viewer/src/sw.ts @@ -49,8 +49,6 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI } catch (error: any) { if (error?.message?.includes('Cannot find .trace file') && await traceModel.hasEntry('index.html')) throw new Error('Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.'); - // eslint-disable-next-line no-console - console.error(error); if (traceFileName) throw new Error(`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`); throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`); diff --git a/packages/trace-viewer/src/ui/workbench.css b/packages/trace-viewer/src/ui/workbench.css index e4ba7cdf77..031039c51d 100644 --- a/packages/trace-viewer/src/ui/workbench.css +++ b/packages/trace-viewer/src/ui/workbench.css @@ -30,9 +30,16 @@ line-height: 24px; } +body .drop-target { + background: rgba(255, 255, 255, 0.8); +} + +body.dark-mode .drop-target { + background: rgba(0, 0, 0, 0.8); +} + .drop-target .title { font-size: 24px; - color: #666; font-weight: bold; margin-bottom: 30px; } @@ -81,7 +88,7 @@ flex-basis: 48px; line-height: 48px; font-size: 16px; - color: white; + color: #cccccc; } .workbench .header .toolbar-button { diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 4917201a90..d23bd5ba74 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -24,6 +24,7 @@ import { Workbench } from './workbench'; export const WorkbenchLoader: React.FunctionComponent<{ }> = () => { + const [isServer, setIsServer] = React.useState(false); const [traceURLs, setTraceURLs] = React.useState([]); const [uploadedTraceNames, setUploadedTraceNames] = React.useState([]); const [model, setModel] = React.useState(emptyModel); @@ -32,7 +33,7 @@ export const WorkbenchLoader: React.FunctionComponent<{ const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null); const [fileForLocalModeError, setFileForLocalModeError] = React.useState(null); - const processTraceFiles = (files: FileList) => { + const processTraceFiles = React.useCallback((files: FileList) => { const blobUrls = []; const fileNames = []; const url = new URL(window.location.href); @@ -54,22 +55,25 @@ export const WorkbenchLoader: React.FunctionComponent<{ setUploadedTraceNames(fileNames); setDragOver(false); setProcessingErrorMessage(null); - }; + }, []); - const handleDropEvent = (event: React.DragEvent) => { + const handleDropEvent = React.useCallback((event: React.DragEvent) => { event.preventDefault(); processTraceFiles(event.dataTransfer.files); - }; + }, [processTraceFiles]); - const handleFileInputChange = (event: any) => { + const handleFileInputChange = React.useCallback((event: any) => { event.preventDefault(); if (!event.target.files) return; processTraceFiles(event.target.files); - }; + }, [processTraceFiles]); React.useEffect(() => { - const newTraceURLs = new URL(window.location.href).searchParams.getAll('trace'); + const params = new URL(window.location.href).searchParams; + const newTraceURLs = params.getAll('trace'); + setIsServer(params.has('isServer')); + // Don't accept file:// URLs - this means we re opened locally. for (const url of newTraceURLs) { if (url.startsWith('file:')) { @@ -80,11 +84,16 @@ export const WorkbenchLoader: React.FunctionComponent<{ (window as any).setTraceURL = (url: string) => { setTraceURLs([url]); + setDragOver(false); + setProcessingErrorMessage(null); }; - // Don't re-use blob file URLs on page load (results in Fetch error) - if (!newTraceURLs.some(url => url.startsWith('blob:'))) + if (earlyTraceURL) { + (window as any).setTraceURL(earlyTraceURL); + } else if (!newTraceURLs.some(url => url.startsWith('blob:'))) { + // Don't re-use blob file URLs on page load (results in Fetch error) setTraceURLs(newTraceURLs); - }, [setTraceURLs]); + } + }, []); React.useEffect(() => { (async () => { @@ -104,7 +113,8 @@ export const WorkbenchLoader: React.FunctionComponent<{ params.set('traceFileName', uploadedTraceNames[i]); const response = await fetch(`contexts?${params.toString()}`); if (!response.ok) { - setTraceURLs([]); + if (!isServer) + setTraceURLs([]); setProcessingErrorMessage((await response.json()).error); return; } @@ -118,7 +128,7 @@ export const WorkbenchLoader: React.FunctionComponent<{ setModel(emptyModel); } })(); - }, [traceURLs, uploadedTraceNames]); + }, [isServer, traceURLs, uploadedTraceNames]); return
{ event.preventDefault(); setDragOver(true); }}>
@@ -128,9 +138,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
toggleTheme()}>
- {!!progress.total &&
-
-
} +
+
+
{fileForLocalModeError &&
Trace Viewer uses Service Workers to show traces. To view trace:
@@ -140,7 +150,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
3. Drop the trace from the download shelf into the page
} - {!dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) &&
+ {!isServer && !dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) &&
{processingErrorMessage}
Drop Playwright Trace to load
or
@@ -153,6 +163,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally.
} + {isServer && (!traceURLs.length || processingErrorMessage) &&
+
Select test to see the trace
+
} {dragOver &&
{ setDragOver(false); }} onDrop={event => handleDropEvent(event)}> @@ -162,3 +175,9 @@ export const WorkbenchLoader: React.FunctionComponent<{ }; export const emptyModel = new MultiTraceModel([]); + +let earlyTraceURL: string | undefined = undefined; + +(window as any).setTraceURL = (url: string) => { + earlyTraceURL = url; +};