From 8b1a887756c36178dc30ea331fbc6f339cc1da9c Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 13 Oct 2021 10:07:29 -0800 Subject: [PATCH] feat(html): link traces from html report (#9473) --- .../src/web/htmlReport/htmlReport.tsx | 45 +++++++++++-------- .../src/web/traceViewer/snapshotRenderer.ts | 2 +- .../playwright-core/src/web/traceViewer/sw.ts | 27 ++++++----- .../src/web/traceViewer/ui/filmStrip.tsx | 6 +-- .../traceViewer/ui/networkResourceDetails.tsx | 4 +- .../src/web/traceViewer/ui/snapshotTab.tsx | 4 +- .../src/web/traceViewer/ui/workbench.tsx | 10 +++-- .../playwright-test/src/reporters/html.ts | 14 +++++- 8 files changed, 70 insertions(+), 42 deletions(-) diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx index 138904073c..4a2422ee03 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx @@ -163,27 +163,28 @@ const TestResultView: React.FC<{ result: TestResult, }> = ({ result }) => { - const { screenshots, videos, otherAttachments, attachmentsMap } = React.useMemo(() => { + const { screenshots, videos, traces, otherAttachments, attachmentsMap } = React.useMemo(() => { const attachmentsMap = new Map(); const attachments = result?.attachments || []; const otherAttachments: TestAttachment[] = []; const screenshots = attachments.filter(a => a.name === 'screenshot'); const videos = attachments.filter(a => a.name === 'video'); - const knownNames = new Set(['screenshot', 'image', 'expected', 'actual', 'diff', 'video']); + const traces = attachments.filter(a => a.name === 'trace'); + const knownNames = new Set(['screenshot', 'image', 'expected', 'actual', 'diff', 'video', 'trace']); for (const a of attachments) { attachmentsMap.set(a.name, a); if (!knownNames.has(a.name)) otherAttachments.push(a); } - return { attachmentsMap, screenshots, videos, otherAttachments }; + return { attachmentsMap, screenshots, videos, otherAttachments, traces }; }, [ result ]); const expected = attachmentsMap.get('expected'); const actual = attachmentsMap.get('actual'); const diff = attachmentsMap.get('diff'); return
- {result.error && } - {result.steps.map((step, i) => )} + {result.error && } + {result.steps.map((step, i) => )} {expected && actual &&
@@ -192,24 +193,29 @@ const TestResultView: React.FC<{ {diff && }
} - {!!screenshots.length &&
Screenshots
} + {!!screenshots.length &&
Screenshots
} {screenshots.map((a, i) => { - return
- - + return
+ +
; })} - {!!videos.length &&
Videos
} - {videos.map((a, i) =>
- - + {!!traces.length &&
Traces
} + {traces.map((a, i) =>
+
)} - {!!otherAttachments &&
Attachments
} - {otherAttachments.map((a, i) => )} + {!!videos.length &&
Videos
} + {videos.map((a, i) =>
+ + +
)} + + {!!otherAttachments &&
Attachments
} + {otherAttachments.map((a, i) => )}
; }; @@ -243,10 +249,11 @@ const StatsView: React.FC<{ export const AttachmentLink: React.FunctionComponent<{ attachment: TestAttachment, -}> = ({ attachment }) => { + href?: string, +}> = ({ attachment, href }) => { return - {attachment.path && {attachment.name}} + {attachment.path && {attachment.name}} {attachment.body && {attachment.name}}
} loadChildren={attachment.body ? () => { return [
${attachment.body}
]; diff --git a/packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts b/packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts index aea85cfc81..88fd844688 100644 --- a/packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts +++ b/packages/playwright-core/src/web/traceViewer/snapshotRenderer.ts @@ -185,7 +185,7 @@ function snapshotScript() { iframe.setAttribute('src', 'data:text/html,'); } else { // Append query parameters to inherit ?name= or ?time= values from parent. - iframe.setAttribute('src', window.location.origin + src + window.location.search); + iframe.setAttribute('src', new URL(src + window.location.search, window.location.href).toString()); } } diff --git a/packages/playwright-core/src/web/traceViewer/sw.ts b/packages/playwright-core/src/web/traceViewer/sw.ts index 93212fadba..b40394290d 100644 --- a/packages/playwright-core/src/web/traceViewer/sw.ts +++ b/packages/playwright-core/src/web/traceViewer/sw.ts @@ -20,7 +20,9 @@ import { TraceModel } from './traceModel'; // @ts-ignore declare const self: ServiceWorkerGlobalScope; -self.addEventListener('install', function(event: any) {}); +self.addEventListener('install', function(event: any) { + self.skipWaiting(); +}); self.addEventListener('activate', function(event: any) { event.waitUntil(self.clients.claim()); @@ -28,6 +30,7 @@ self.addEventListener('activate', function(event: any) { let traceModel: TraceModel | undefined; let snapshotServer: SnapshotServer | undefined; +const scopePath = new URL(self.registration.scope).pathname; async function loadTrace(trace: string): Promise { const traceModel = new TraceModel(); @@ -39,13 +42,14 @@ async function loadTrace(trace: string): Promise { // @ts-ignore async function doFetch(event: FetchEvent): Promise { const request = event.request; - const { pathname, searchParams } = new URL(request.url); + const url = new URL(request.url); const snapshotUrl = request.mode === 'navigate' ? request.url : (await self.clients.get(event.clientId))!.url; - if (request.url.startsWith(self.location.origin)) { - if (pathname === '/context') { - const trace = searchParams.get('trace')!; + if (request.url.startsWith(self.registration.scope)) { + 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()); return new Response(JSON.stringify(traceModel!.contextEntry), { @@ -53,12 +57,12 @@ async function doFetch(event: FetchEvent): Promise { headers: { 'Content-Type': 'application/json' } }); } - if (pathname.startsWith('/snapshotSize/')) - return snapshotServer!.serveSnapshotSize(pathname, searchParams); - if (pathname.startsWith('/snapshot/')) - return snapshotServer!.serveSnapshot(pathname, searchParams, snapshotUrl); - if (pathname.startsWith('/sha1/')) { - const blob = await traceModel!.resourceForSha1(pathname.slice('/sha1/'.length)); + 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 @@ -67,6 +71,7 @@ async function doFetch(event: FetchEvent): Promise { return fetch(event.request); } + if (!snapshotServer) return new Response(null, { status: 404 }); return snapshotServer!.serveResource(request.url, snapshotUrl); diff --git a/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx b/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx index 497c214db6..e8ea376298 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/filmStrip.tsx @@ -61,7 +61,7 @@ export const FilmStrip: React.FunctionComponent<{ top: measure.bottom + 5, left: Math.min(previewPoint!.x, measure.width - previewSize.width - 10), }}> - +
} ; @@ -97,7 +97,7 @@ const FilmStripLane: React.FunctionComponent<{ frames.push(
{ if (resource.request.postData) { if (resource.request.postData._sha1) { - const response = await fetch(`/sha1/${resource.request.postData._sha1}`); + const response = await fetch(`sha1/${resource.request.postData._sha1}`); const requestResource = await response.text(); setRequestBody(requestResource); } else { @@ -48,7 +48,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{ if (resource.response.content._sha1) { const useBase64 = resource.response.content.mimeType.includes('image'); - const response = await fetch(`/sha1/${resource.response.content._sha1}`); + const response = await fetch(`sha1/${resource.response.content._sha1}`); if (useBase64) { const blob = await response.blob(); const reader = new FileReader(); diff --git a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx index 1a1e831e81..b0b460d1da 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx @@ -41,8 +41,8 @@ export const SnapshotTab: React.FunctionComponent<{ if (action) { const snapshot = snapshots[snapshotIndex]; if (snapshot && snapshot.snapshotName) { - snapshotUrl = `${window.location.origin}/snapshot/${action.metadata.pageId}?name=${snapshot.snapshotName}`; - snapshotSizeUrl = `${window.location.origin}/snapshotSize/${action.metadata.pageId}?name=${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(); if (snapshot.snapshotName.includes('action')) { pointX = action.metadata.point?.x; pointY = action.metadata.point?.y; diff --git a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx index 7e06cc6ab0..c43439a795 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx @@ -39,9 +39,13 @@ export const Workbench: React.FunctionComponent<{ React.useEffect(() => { (async () => { - const contextEntry = (await fetch(`/context?trace=${traceURL}`).then(response => response.json())) as ContextEntry; - modelUtil.indexModel(contextEntry); - setContextEntry(contextEntry); + if (traceURL) { + const contextEntry = (await fetch(`context?trace=${traceURL}`).then(response => response.json())) as ContextEntry; + modelUtil.indexModel(contextEntry); + setContextEntry(contextEntry); + } else { + setContextEntry(emptyContext); + } })(); }, [traceURL]); diff --git a/packages/playwright-test/src/reporters/html.ts b/packages/playwright-test/src/reporters/html.ts index 6d714c5f7d..1c24f98085 100644 --- a/packages/playwright-test/src/reporters/html.ts +++ b/packages/playwright-test/src/reporters/html.ts @@ -121,7 +121,7 @@ class HtmlReporter { if (!process.env.CI && !process.env.PWTEST_SKIP_TEST_OUTPUT) { const server = new HttpServer(); server.routePrefix('/', (request, response) => { - let relativePath = request.url!; + let relativePath = new URL('http://localhost' + request.url).pathname; if (relativePath === '/') relativePath = '/index.html'; const absolutePath = path.join(reportFolder, ...relativePath.split('/')); @@ -149,10 +149,22 @@ class HtmlBuilder { this._reportFolder = path.resolve(process.cwd(), outputDir); this._dataFolder = path.join(this._reportFolder, 'data'); fs.mkdirSync(this._dataFolder, { recursive: true }); + + // Copy app. const appFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'web', 'htmlReport'); for (const file of fs.readdirSync(appFolder)) fs.copyFileSync(path.join(appFolder, file), path.join(this._reportFolder, file)); + // Copy trace viewer. + const traceViewerFolder = path.join(require.resolve('playwright-core'), '..', 'lib', 'web', 'traceViewer'); + const traceViewerTargetFolder = path.join(this._reportFolder, 'trace'); + fs.mkdirSync(traceViewerTargetFolder, { recursive: true }); + // TODO (#9471): remove file filter when the babel build is fixed. + for (const file of fs.readdirSync(traceViewerFolder)) { + if (fs.statSync(path.join(traceViewerFolder, file)).isFile()) + fs.copyFileSync(path.join(traceViewerFolder, file), path.join(traceViewerTargetFolder, file)); + } + const projects: ProjectTreeItem[] = []; for (const projectJson of rawReports) { const suites: SuiteTreeItem[] = [];