();
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[] = [];