diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index e1b7ad9196..1f46497086 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -218,7 +218,7 @@ program options.browser = 'firefox'; if (options.browser === 'wk') options.browser = 'webkit'; - showTraceViewer(trace, options.browser).catch(logErrorAndExit); + showTraceViewer(trace, options.browser, false, 9322).catch(logErrorAndExit); }).addHelpText('afterAll', ` Examples: diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 96cc10bd0c..a452d48d7c 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -25,7 +25,7 @@ import { internalCallMetadata } from '../../instrumentation'; import { createPlaywright } from '../../playwright'; import { ProgressController } from '../../progress'; -export async function showTraceViewer(traceUrl: string, browserName: string, headless = false): Promise { +export async function showTraceViewer(traceUrl: string, browserName: string, headless = false, port?: number): Promise { const server = new HttpServer(); server.routePath('/file', (request, response) => { try { @@ -42,7 +42,7 @@ export async function showTraceViewer(traceUrl: string, browserName: string, hea return server.serveFile(response, absolutePath); }); - const urlPrefix = await server.start(); + const urlPrefix = await server.start(port); const traceViewerPlaywright = createPlaywright('javascript', true); const traceViewerBrowser = isUnderTest() ? 'chromium' : browserName; diff --git a/packages/playwright-core/src/web/traceViewer/sw.ts b/packages/playwright-core/src/web/traceViewer/sw.ts index b40394290d..8e7880904d 100644 --- a/packages/playwright-core/src/web/traceViewer/sw.ts +++ b/packages/playwright-core/src/web/traceViewer/sw.ts @@ -28,53 +28,86 @@ self.addEventListener('activate', function(event: any) { event.waitUntil(self.clients.claim()); }); -let traceModel: TraceModel | undefined; -let snapshotServer: SnapshotServer | undefined; const scopePath = new URL(self.registration.scope).pathname; -async function loadTrace(trace: string): Promise { +const loadedTraces = new Map(); + +async function loadTrace(trace: string, clientId: string): Promise { + const entry = loadedTraces.get(trace); + if (entry) + return entry.traceModel; const traceModel = new TraceModel(); const url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `/file?path=${trace}`; await traceModel.load(url); + const snapshotServer = new SnapshotServer(traceModel.storage()); + loadedTraces.set(trace, { traceModel, snapshotServer, clientId }); return traceModel; } // @ts-ignore async function doFetch(event: FetchEvent): Promise { const request = event.request; - const url = new URL(request.url); const snapshotUrl = request.mode === 'navigate' ? request.url : (await self.clients.get(event.clientId))!.url; + const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!; + const { snapshotServer } = loadedTraces.get(traceUrl) || {}; if (request.url.startsWith(self.registration.scope)) { + const url = new URL(request.url); + const relativePath = url.pathname.substring(scopePath.length - 1); if (relativePath === '/context') { - const trace = url.searchParams.get('trace')!; - traceModel = await loadTrace(trace); - snapshotServer = new SnapshotServer(traceModel.storage()); + await gc(); + const traceModel = await loadTrace(traceUrl, event.clientId); return new Response(JSON.stringify(traceModel!.contextEntry), { status: 200, headers: { 'Content-Type': 'application/json' } }); } - if (relativePath.startsWith('/snapshotSize/')) - return snapshotServer!.serveSnapshotSize(relativePath, url.searchParams); - if (relativePath.startsWith('/snapshot/')) - return snapshotServer!.serveSnapshot(relativePath, url.searchParams, snapshotUrl); - if (relativePath.startsWith('/sha1/')) { - const blob = await traceModel!.resourceForSha1(relativePath.slice('/sha1/'.length)); - if (blob) - return new Response(blob, { status: 200 }); - else + + if (relativePath.startsWith('/snapshotSize/')) { + if (!snapshotServer) return new Response(null, { status: 404 }); + return snapshotServer.serveSnapshotSize(relativePath, url.searchParams); } + + if (relativePath.startsWith('/snapshot/')) { + if (!snapshotServer) + return new Response(null, { status: 404 }); + return snapshotServer.serveSnapshot(relativePath, url.searchParams, snapshotUrl); + } + + if (relativePath.startsWith('/sha1/')) { + // Sha1 is unique, load it from either of the models for simplicity. + for (const { traceModel } of loadedTraces.values()) { + const blob = await traceModel!.resourceForSha1(relativePath.slice('/sha1/'.length)); + if (blob) + return new Response(blob, { status: 200 }); + } + return new Response(null, { status: 404 }); + } + + // Fallback to network. return fetch(event.request); } - if (!snapshotServer) return new Response(null, { status: 404 }); - return snapshotServer!.serveResource(request.url, snapshotUrl); + return snapshotServer.serveResource(request.url, snapshotUrl); +} + +async function gc() { + const usedTraces = new Set(); + for (const [traceUrl, entry] of loadedTraces) { + const client = await self.clients.get(entry.clientId); + if (client) + usedTraces.add(traceUrl); + } + + for (const traceUrl of loadedTraces.keys()) { + if (!usedTraces.has(traceUrl)) + loadedTraces.delete(traceUrl); + } } // @ts-ignore diff --git a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx index b0b460d1da..edce0c1ddd 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx @@ -41,8 +41,9 @@ export const SnapshotTab: React.FunctionComponent<{ if (action) { const snapshot = snapshots[snapshotIndex]; if (snapshot && snapshot.snapshotName) { - snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?name=${snapshot.snapshotName}`, window.location.href).toString(); - snapshotSizeUrl = new URL(`snapshotSize/${action.metadata.pageId}?name=${snapshot.snapshotName}`, window.location.href).toString(); + const traceUrl = new URL(window.location.href).searchParams.get('trace'); + snapshotUrl = new URL(`snapshot/${action.metadata.pageId}?trace=${traceUrl}&name=${snapshot.snapshotName}`, window.location.href).toString(); + snapshotSizeUrl = new URL(`snapshotSize/${action.metadata.pageId}?trace=${traceUrl}&name=${snapshot.snapshotName}`, window.location.href).toString(); if (snapshot.snapshotName.includes('action')) { pointX = action.metadata.point?.x; pointY = action.metadata.point?.y; diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index 1c24f98085..dc4231a77c 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -127,7 +127,7 @@ class HtmlReporter { const absolutePath = path.join(reportFolder, ...relativePath.split('/')); return server.serveFile(response, absolutePath); }); - const url = await server.start(); + const url = await server.start(9323); console.log(''); console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`)); console.log('');