diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index b8dc3e5314..b1cc8141a4 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -125,6 +125,8 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ for (const reporter of options.reporter || []) params.append('reporter', reporter); + params.set('server', server.urlPrefix('precise')); + const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`; server.routePath('/', (_, response) => { response.statusCode = 302; diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 6fcb08daa7..d977b0d7b7 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -18,7 +18,7 @@ import { splitProgress } from './progress'; import { unwrapPopoutUrl } from './snapshotRenderer'; import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; -import { FetchTraceModelBackend, ZipTraceModelBackend } from './traceModelBackends'; +import { FetchTraceModelBackend, TraceViewerServer, ZipTraceModelBackend } from './traceModelBackends'; import { TraceVersionError } from './traceModernizer'; // @ts-ignore @@ -36,13 +36,21 @@ const scopePath = new URL(self.registration.scope).pathname; const loadedTraces = new Map(); -const clientIdToTraceUrls = new Map }>(); +const clientIdToTraceUrls = new Map, traceViewerServer: TraceViewerServer }>(); -async function loadTrace(traceUrl: string, traceFileName: string | null, clientId: string, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { +async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { await gc(); + const clientId = client?.id ?? ''; let data = clientIdToTraceUrls.get(clientId); if (!data) { - data = { limit, traceUrls: new Set() }; + let traceViewerServerBaseUrl = self.registration.scope; + if (client?.url) { + const clientUrl = new URL(client.url); + if (clientUrl.searchParams.has('server')) + traceViewerServerBaseUrl = clientUrl.searchParams.get('server')!; + } + + data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) }; clientIdToTraceUrls.set(clientId, data); } data.traceUrls.add(traceUrl); @@ -51,7 +59,7 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI try { // Allow 10% to hop from sw to page. const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); - const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); + const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl, data.traceViewerServer) : new ZipTraceModelBackend(traceUrl, data.traceViewerServer, fetchProgress); await traceModel.load(backend, unzipProgress); } catch (error: any) { // eslint-disable-next-line no-console @@ -98,7 +106,7 @@ async function doFetch(event: FetchEvent): Promise { if (relativePath === '/contexts') { try { const limit = url.searchParams.has('limit') ? +url.searchParams.get('limit')! : undefined; - const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), event.clientId, limit, (done: number, total: number) => { + const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, limit, (done: number, total: number) => { client.postMessage({ method: 'progress', params: { done, total } }); }); return new Response(JSON.stringify(traceModel!.contextEntries), { @@ -148,7 +156,18 @@ async function doFetch(event: FetchEvent): Promise { return new Response(null, { status: 404 }); } - // Fallback to network. + if (relativePath.startsWith('/file/')) { + const path = url.searchParams.get('path')!; + const traceViewerServer = clientIdToTraceUrls.get(event.clientId ?? '')?.traceViewerServer; + if (!traceViewerServer) + throw new Error('client is not initialized'); + const response = await traceViewerServer.readFile(path); + if (!response) + return new Response(null, { status: 404 }); + return response; + } + + // Fallback for static assets. return fetch(event.request); } diff --git a/packages/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts index 19c5fc2dee..ec367575e3 100644 --- a/packages/trace-viewer/src/sw/traceModelBackends.ts +++ b/packages/trace-viewer/src/sw/traceModelBackends.ts @@ -28,11 +28,11 @@ export class ZipTraceModelBackend implements TraceModelBackend { private _entriesPromise: Promise>; private _traceURL: string; - constructor(traceURL: string, progress: Progress) { + constructor(traceURL: string, server: TraceViewerServer, progress: Progress) { this._traceURL = traceURL; zipjs.configure({ baseURL: self.location.href } as any); this._zipReader = new zipjs.ZipReader( - new zipjs.HttpReader(formatUrl(traceURL), { mode: 'cors', preventHeadRequest: true } as any), + new zipjs.HttpReader(formatUrl(traceURL, server), { mode: 'cors', preventHeadRequest: true } as any), { useWebWorkers: false }); this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => { const map = new Map(); @@ -83,12 +83,16 @@ export class ZipTraceModelBackend implements TraceModelBackend { export class FetchTraceModelBackend implements TraceModelBackend { private _entriesPromise: Promise>; - private _traceURL: string; + private _path: string; + private _server: TraceViewerServer; - constructor(traceURL: string) { - this._traceURL = traceURL; - this._entriesPromise = fetch('/trace/file?path=' + encodeURIComponent(traceURL)).then(async response => { - const json = JSON.parse(await response.text()); + constructor(path: string, server: TraceViewerServer) { + this._path = path; + this._server = server; + this._entriesPromise = server.readFile(path).then(async response => { + if (!response) + throw new Error('File not found'); + const json = await response.json(); const entries = new Map(); for (const entry of json.entries) entries.set(entry.name, entry.path); @@ -101,7 +105,7 @@ export class FetchTraceModelBackend implements TraceModelBackend { } traceURL(): string { - return this._traceURL; + return this._path; } async entryNames(): Promise { @@ -129,14 +133,31 @@ export class FetchTraceModelBackend implements TraceModelBackend { const fileName = entries.get(entryName); if (!fileName) return; - return fetch('/trace/file?path=' + encodeURIComponent(fileName)); + return this._server.readFile(fileName); } } -function formatUrl(trace: string) { - let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${encodeURIComponent(trace)}`; +function formatUrl(trace: string, server: TraceViewerServer) { + let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : server.getFileURL(trace).toString(); // Dropbox does not support cors. if (url.startsWith('https://www.dropbox.com/')) url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length); return url; } + +export class TraceViewerServer { + constructor(private readonly baseUrl: string) {} + + getFileURL(path: string): URL { + const url = new URL('trace/file', this.baseUrl); + url.searchParams.set('path', path); + return url; + } + + async readFile(path: string): Promise { + const response = await fetch(this.getFileURL(path)); + if (response.status === 404) + return; + return response; + } +} \ No newline at end of file diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index bbf0e763da..2e8bf483ba 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -328,6 +328,7 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot } const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest'); +const serverParam = new URLSearchParams(window.location.search).get('server'); export function extendSnapshot(snapshot: Snapshot): SnapshotUrls { const params = new URLSearchParams(); @@ -346,6 +347,7 @@ export function extendSnapshot(snapshot: Snapshot): SnapshotUrls { const popoutParams = new URLSearchParams(); popoutParams.set('r', snapshotUrl); + popoutParams.set('server', serverParam ?? ''); popoutParams.set('trace', context(snapshot.action).traceUrl); if (snapshot.point) { popoutParams.set('pointX', String(snapshot.point.x));