diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 610ba1b567..07aa9a0af2 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -38,6 +38,8 @@ export async function showTraceViewer(traceUrls: string[], browserName: string, server.routePrefix('/trace', (request, response) => { const url = new URL('http://localhost' + request.url!); const relativePath = url.pathname.slice('/trace'.length); + if (relativePath.endsWith('/stall.js')) + return true; if (relativePath.startsWith('/file')) { try { return server.serveFile(request, response, url.searchParams.get('path')!); diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index b9329d0888..b85bdff2d1 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -181,6 +181,8 @@ export function startHtmlReportServer(folder: string): HttpServer { return false; } } + if (relativePath.endsWith('/stall.js')) + return true; if (relativePath === '/') relativePath = '/index.html'; const absolutePath = path.join(folder, ...relativePath.split('/')); diff --git a/packages/trace-viewer/bundle.js b/packages/trace-viewer/bundle.js index 6a9f93b27d..cf02d28cfb 100644 --- a/packages/trace-viewer/bundle.js +++ b/packages/trace-viewer/bundle.js @@ -31,7 +31,8 @@ export function bundle() { transform(html, ctx) { if (!ctx || !ctx.bundle) return html; - return html.replace(/(?=/, ''); + // Workaround vite issue that we cannot exclude some scripts from preprocessing. + return html.replace(/(?=/, '').replace('', ''); }, }, } diff --git a/packages/trace-viewer/popout.html b/packages/trace-viewer/popout.html new file mode 100644 index 0000000000..c1de1ea8ab --- /dev/null +++ b/packages/trace-viewer/popout.html @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/packages/trace-viewer/src/snapshotRenderer.ts b/packages/trace-viewer/src/snapshotRenderer.ts index 20216a9811..86fd4a0019 100644 --- a/packages/trace-viewer/src/snapshotRenderer.ts +++ b/packages/trace-viewer/src/snapshotRenderer.ts @@ -180,7 +180,7 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] { } function snapshotScript() { - function applyPlaywrightAttributes() { + function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string) { const scrollTops: Element[] = []; const scrollLefts: Element[] = []; @@ -210,7 +210,7 @@ function snapshotScript() { iframe.setAttribute('src', 'data:text/html,'); } else { // Append query parameters to inherit ?name= or ?time= values from parent. - const url = new URL(window.location.href); + const url = new URL(unwrapPopoutUrl(window.location.href)); url.searchParams.delete('pointX'); url.searchParams.delete('pointY'); // We can be loading iframe from within iframe, reset base to be absolute. @@ -278,7 +278,7 @@ function snapshotScript() { window.addEventListener('DOMContentLoaded', onDOMContentLoaded); } - return `\n(${applyPlaywrightAttributes.toString()})()`; + return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()})`; } @@ -329,3 +329,11 @@ function rewriteURLsInStyleSheetForCustomProtocol(text: string): string { return match.replace(protocol + '//', `https://pw-${protocol.slice(0, -1)}--`); }); } + +// /popout.html?r= is used for "pop out snapshot" feature. +export function unwrapPopoutUrl(url: string) { + const u = new URL(url); + if (u.pathname.endsWith('/popout.html')) + return u.searchParams.get('r')!; + return url; +} diff --git a/packages/trace-viewer/src/sw.ts b/packages/trace-viewer/src/sw.ts index e3d69fdf12..c5992d8619 100644 --- a/packages/trace-viewer/src/sw.ts +++ b/packages/trace-viewer/src/sw.ts @@ -15,6 +15,7 @@ */ import { MultiMap } from './multimap'; +import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; @@ -65,7 +66,7 @@ async function doFetch(event: FetchEvent): Promise { const client = await self.clients.get(event.clientId); if (request.url.startsWith(self.registration.scope)) { - const url = new URL(request.url); + const url = new URL(unwrapPopoutUrl(request.url)); const relativePath = url.pathname.substring(scopePath.length - 1); if (relativePath === '/ping') { await gc(); @@ -101,7 +102,7 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath.startsWith('/snapshot/')) { if (!snapshotServer) return new Response(null, { status: 404 }); - return snapshotServer.serveSnapshot(relativePath, url.searchParams, request.url); + return snapshotServer.serveSnapshot(relativePath, url.searchParams, url.href); } if (relativePath.startsWith('/sha1/')) { @@ -118,7 +119,7 @@ async function doFetch(event: FetchEvent): Promise { return fetch(event.request); } - const snapshotUrl = client!.url; + const snapshotUrl = unwrapPopoutUrl(client!.url); const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!; const { snapshotServer } = loadedTraces.get(traceUrl) || {}; if (!snapshotServer) diff --git a/packages/trace-viewer/src/ui/snapshotTab.css b/packages/trace-viewer/src/ui/snapshotTab.css index 1bfeddd9be..b023ca3f37 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.css +++ b/packages/trace-viewer/src/ui/snapshotTab.css @@ -63,6 +63,7 @@ flex: auto; margin: 1px; padding: 10px; + position: relative; } .snapshot-container { @@ -82,6 +83,22 @@ iframe#snapshot { padding: 50px; } +.popout-icon { + position: absolute; + top: 10px; + right: 10px; + color: var(--gray); + font-size: 14px; +} + +.popout-icon:not(.popout-disabled):hover { + color: var(--blue); +} + +.popout-icon.popout-disabled { + opacity: 0.7; +} + .window-dot { border-radius: 50%; display: inline-block; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index b080e0df8b..f151cc7500 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -34,6 +34,7 @@ export const SnapshotTab: React.FunctionComponent<{ const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[]; let snapshotUrl = 'data:text/html,'; + let popoutUrl: string | undefined; let snapshotInfoUrl: string | undefined; let pointX: number | undefined; let pointY: number | undefined; @@ -49,6 +50,10 @@ export const SnapshotTab: React.FunctionComponent<{ pointX = action.metadata.point?.x; pointY = action.metadata.point?.y; } + const popoutParams = new URLSearchParams(); + popoutParams.set('r', snapshotUrl); + popoutParams.set('trace', context(action).traceUrl); + popoutUrl = new URL(`popout.html?${popoutParams.toString()}`, window.location.href).toString(); } } @@ -107,6 +112,9 @@ export const SnapshotTab: React.FunctionComponent<{ })}
+ + + { snapshots.length ?
'[name].[hash].js', assetFileNames: () => '[name].[hash][extname]', diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index c404c6bd7f..3c688e199c 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -223,6 +223,18 @@ test('should show snapshot URL', async ({ page, runAndTrace, server }) => { await expect(traceViewer.page.locator('.window-address-bar')).toHaveText(server.EMPTY_PAGE); }); +test('should popup snapshot', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.EMPTY_PAGE); + await page.setContent('hello'); + }); + await traceViewer.snapshotFrame('page.setContent'); + const popupPromise = traceViewer.page.context().waitForEvent('page'); + await traceViewer.page.getByTitle('Open snapshot in a new tab').click(); + const popup = await popupPromise; + await expect(popup.getByText('hello')).toBeVisible(); +}); + test('should capture iframe with sandbox attribute', async ({ page, server, runAndTrace }) => { await page.route('**/empty.html', route => { route.fulfill({