diff --git a/packages/isomorphic/trace/entries.ts b/packages/isomorphic/trace/entries.ts index cd902c9f118c2..91fb0932583ea 100644 --- a/packages/isomorphic/trace/entries.ts +++ b/packages/isomorphic/trace/entries.ts @@ -48,7 +48,7 @@ export type ContextEntry = { export type PageEntry = { pageId: string, screencastFrames: { - sha1: string, + file: string, timestamp: number, frameSwapWallTime?: number, width: number, diff --git a/packages/isomorphic/trace/snapshotRenderer.ts b/packages/isomorphic/trace/snapshotRenderer.ts index 7001dea03f89a..2c9284987015d 100644 --- a/packages/isomorphic/trace/snapshotRenderer.ts +++ b/packages/isomorphic/trace/snapshotRenderer.ts @@ -71,7 +71,7 @@ export class SnapshotRenderer { const closestFrame = (wallTime && this._screencastFrames[0]?.frameSwapWallTime) ? findClosest(this._screencastFrames, frame => frame.frameSwapWallTime!, wallTime) : findClosest(this._screencastFrames, frame => frame.timestamp, timestamp); - return closestFrame?.sha1; + return closestFrame?.file; } render(): RenderedFrameSnapshot { @@ -234,14 +234,14 @@ export class SnapshotRenderer { if (index >= 0 && index < this._snapshots.length) override = this._snapshots[index].resourceOverrides.find(o => o.url === url); } - if (override?.sha1) { + if (override?.file) { result = { ...result, response: { ...result.response, content: { ...result.response.content, - _sha1: override.sha1, + _file: override.file, } }, }; diff --git a/packages/isomorphic/trace/snapshotServer.ts b/packages/isomorphic/trace/snapshotServer.ts index e2703a0060fc3..8149a10f85d78 100644 --- a/packages/isomorphic/trace/snapshotServer.ts +++ b/packages/isomorphic/trace/snapshotServer.ts @@ -21,10 +21,10 @@ import type { ResourceSnapshot } from '@trace/snapshot'; export class SnapshotServer { private _snapshotStorage: SnapshotStorage; - private _resourceLoader: (sha1: string) => Promise; + private _resourceLoader: (file: string) => Promise; private _snapshotIds = new Map(); - constructor(snapshotStorage: SnapshotStorage, resourceLoader: (sha1: string) => Promise) { + constructor(snapshotStorage: SnapshotStorage, resourceLoader: (file: string) => Promise) { this._snapshotStorage = snapshotStorage; this._resourceLoader = resourceLoader; } @@ -41,10 +41,10 @@ export class SnapshotServer { async serveClosestScreenshot(pageOrFrameId: string, searchParams: URLSearchParams): Promise { const snapshot = this._snapshot(pageOrFrameId, searchParams); - const sha1 = snapshot?.closestScreenshot(); - if (!sha1) + const file = snapshot?.closestScreenshot(); + if (!file) return new Response(null, { status: 404 }); - return new Response(await this._resourceLoader(sha1)); + return new Response(await this._resourceLoader(file)); } serveSnapshotInfo(pageOrFrameId: string, searchParams: URLSearchParams): Response { @@ -85,8 +85,8 @@ export class SnapshotServer { if (!resource) return new Response(null, { status: 404 }); - const sha1 = resource.response.content._sha1; - const content = sha1 ? await this._resourceLoader(sha1) || new Blob([]) : new Blob([]); + const file = resource.response.content._file; + const content = file ? await this._resourceLoader(file) || new Blob([]) : new Blob([]); let contentType = resource.response.content.mimeType; const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType); diff --git a/packages/isomorphic/trace/traceLoader.ts b/packages/isomorphic/trace/traceLoader.ts index 8547a32e142f0..a8a3a1b088f1e 100644 --- a/packages/isomorphic/trace/traceLoader.ts +++ b/packages/isomorphic/trace/traceLoader.ts @@ -48,7 +48,7 @@ export class TraceLoader { const match = entryName.match(/(.+)\.trace$/); if (match && (!prefix || prefix === match[1])) prefixes.push(match[1] || ''); - if (entryName.includes('src@')) + if (entryName.startsWith('src/') || entryName.includes('src@')) hasSource = true; } if (!prefixes.length) @@ -97,10 +97,10 @@ export class TraceLoader { unzipProgress?.(++done, total); for (const resource of contextEntry.resources) { - if (resource.request.postData?._sha1) - this._resourceToContentType.set(resource.request.postData._sha1, stripEncodingFromContentType(resource.request.postData.mimeType)); - if (resource.response.content?._sha1) - this._resourceToContentType.set(resource.response.content._sha1, stripEncodingFromContentType(resource.response.content.mimeType)); + if (resource.request.postData?._file) + this._resourceToContentType.set(resource.request.postData._file, stripEncodingFromContentType(resource.request.postData.mimeType)); + if (resource.response.content?._file) + this._resourceToContentType.set(resource.response.content._file, stripEncodingFromContentType(resource.response.content.mimeType)); } this.contextEntries.push(contextEntry); @@ -113,9 +113,9 @@ export class TraceLoader { return this._backend.hasEntry(filename); } - async resourceForSha1(sha1: string): Promise { - const blob = await this._backend.readBlob('resources/' + sha1); - const contentType = this._resourceToContentType.get(sha1); + async resourceEntry(file: string): Promise { + const blob = await this._backend.readBlob(file); + const contentType = this._resourceToContentType.get(file); // "x-unknown" in the har means "no content type". if (!blob || contentType === undefined || contentType === 'x-unknown') return blob; diff --git a/packages/isomorphic/trace/traceModernizer.ts b/packages/isomorphic/trace/traceModernizer.ts index b18662b16e24b..5b3a43b8bf49b 100644 --- a/packages/isomorphic/trace/traceModernizer.ts +++ b/packages/isomorphic/trace/traceModernizer.ts @@ -200,9 +200,48 @@ export class TraceModernizer { let events = [event]; for (; version < latestVersion; ++version) events = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, events); + for (const e of events) + this._normalizeResourceReferences(e); return events; } + // Traces recorded before trace-relative paths referenced blobs by bare sha1-style names: + // `_sha1` in har entry content, `sha1` in snapshot resource overrides, screencast frames + // and attachments. + private _normalizeResourceReferences(event: any) { + if (event.type === 'resource-snapshot') { + const { request, response } = event.snapshot; + if (request?.postData?._sha1) { + request.postData._file = 'resources/' + request.postData._sha1; + delete request.postData._sha1; + } + if (response?.content?._sha1) { + response.content._file = 'resources/' + response.content._sha1; + delete response.content._sha1; + } + } + if (event.type === 'frame-snapshot') { + for (const override of event.snapshot.resourceOverrides || []) { + if (override.sha1) { + override.file = 'resources/' + override.sha1; + delete override.sha1; + } + } + } + if (event.type === 'screencast-frame' && event.sha1) { + event.file = 'resources/' + event.sha1; + delete event.sha1; + } + if (event.type === 'after' || event.type === 'action') { + for (const attachment of event.attachments || []) { + if (attachment.sha1) { + attachment.file = 'resources/' + attachment.sha1; + delete attachment.sha1; + } + } + } + } + _modernize_0_to_1(events: any[]): any[] { for (const event of events) { if (event.type !== 'action') diff --git a/packages/playwright-core/src/server/har/harRecorder.ts b/packages/playwright-core/src/server/har/harRecorder.ts index 78ce1fe0f8d74..360cfd34df6de 100644 --- a/packages/playwright-core/src/server/har/harRecorder.ts +++ b/packages/playwright-core/src/server/har/harRecorder.ts @@ -33,6 +33,7 @@ export class HarRecorder implements HarTracerDelegate { private _fs = new SerializedFS(); private _harFilePath: string; private _resourcesDir: string; + private _relativeResourcesDir: string; private _isFlushed: boolean = false; private _tracer: HarTracer; private _entries: har.Entry[] = []; @@ -42,18 +43,23 @@ export class HarRecorder implements HarTracerDelegate { this._context = context; const isServer = !!context.attribution.playwright.options.isServer; this._harFilePath = !isServer && options.harPath ? options.harPath : path.join(fallbackDir, `${harId}.har`); - if (!isServer && options.resourcesDir) + const harFileDir = path.dirname(this._harFilePath); + if (!isServer && options.resourcesDir) { this._resourcesDir = options.resourcesDir; - else if (!isServer && options.harPath) - this._resourcesDir = path.dirname(options.harPath); - else + this._relativeResourcesDir = path.relative(harFileDir, this._resourcesDir).split(path.sep).join('/'); + } else if (!isServer && options.harPath) { + this._resourcesDir = harFileDir; + this._relativeResourcesDir = ''; + } else { + // Staging layout for the zip archive, where resources end up next to har.har. this._resourcesDir = path.join(fallbackDir, `${harId}-resources`); + this._relativeResourcesDir = ''; + } const urlFilterRe = options.urlRegexSource !== undefined && options.urlRegexFlags !== undefined ? new RegExp(options.urlRegexSource, options.urlRegexFlags) : undefined; const content = options.content || 'embed'; this._tracer = new HarTracer(context, page, this, { content, slimMode: options.mode === 'minimal', - includeTraceInfo: false, recordRequestOverrides: true, waitForContentOnStop: true, urlFilter: urlFilterRe ?? options.urlGlob, @@ -69,20 +75,28 @@ export class HarRecorder implements HarTracerDelegate { onEntryFinished(entry: har.Entry) { } - onContentBlob(sha1: string, buffer: Buffer) { - if (this._writtenContentEntries.has(sha1)) - return; + onContentBlob(shortName: string, buffer: Buffer): string { + const fullName = this._harRelativePath(shortName); + if (this._writtenContentEntries.has(shortName)) + return fullName; if (!this._writtenContentEntries.size) this._fs.mkdir(this._resourcesDir); - this._writtenContentEntries.add(sha1); - this._fs.writeFile(path.join(this._resourcesDir, sha1), buffer, true /* skipIfExists */); + this._writtenContentEntries.add(shortName); + this._fs.writeFile(path.join(this._resourcesDir, shortName), buffer, true /* skipIfExists */); + return fullName; } - onContentBlobAppend(sha1: string, text: string) { + onContentBlobAppend(shortName: string, text: string) { + const fullName = this._harRelativePath(shortName); if (!this._writtenContentEntries.size) this._fs.mkdir(this._resourcesDir); - this._writtenContentEntries.add(sha1); - this._fs.appendFile(path.join(this._resourcesDir, sha1), text); + this._writtenContentEntries.add(shortName); + this._fs.appendFile(path.join(this._resourcesDir, shortName), text); + return fullName; + } + + private _harRelativePath(shortName: string): string { + return this._relativeResourcesDir ? this._relativeResourcesDir + '/' + shortName : shortName; } private async _flush() { diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 4817c94112edc..334b8b9699539 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -44,13 +44,12 @@ const FALLBACK_HTTP_VERSION = 'HTTP/1.1'; export interface HarTracerDelegate { onEntryStarted(entry: har.Entry): void; onEntryFinished(entry: har.Entry): void; - onContentBlob(sha1: string, buffer: Buffer): void; - onContentBlobAppend(sha1: string, text: string): void; + onContentBlob(shortName: string, buffer: Buffer): string; + onContentBlobAppend(shortName: string, text: string): string; } type HarTracerOptions = { content: 'omit' | 'attach' | 'embed'; - includeTraceInfo: boolean; recordRequestOverrides: boolean; waitForContentOnStop: boolean; urlFilter?: string | RegExp; @@ -446,7 +445,7 @@ export class HarTracer { const harEntry = createHarEntry(pageEntry?.id, method, url, page.mainFrame().guid, this._options, webSocket.wallTimeMs()); harEntry._resourceType = 'websocket'; - let sha1: string | undefined = undefined; + const shortName = createGuid() + '.jsonl'; const recordMessage = (type: 'send' | 'receive', opcode: number, data: string, wallTimeMs: number) => { if (this._omitWebSocketFrames) return; @@ -455,16 +454,9 @@ export class HarTracer { harEntry._webSocketMessages ??= []; harEntry._webSocketMessages.push(message); } else if (this._options.content === 'attach') { - if (!sha1) { - sha1 = createGuid() + '.jsonl'; - if (this._options.includeTraceInfo) - harEntry.response.content._sha1 = sha1; - else - harEntry.response.content._file = sha1; - } if (this._started) - this._delegate.onContentBlobAppend(sha1, JSON.stringify(message) + '\n'); + harEntry.response.content._file = this._delegate.onContentBlobAppend(shortName, JSON.stringify(message) + '\n'); } }; @@ -561,13 +553,9 @@ export class HarTracer { content.encoding = 'base64'; } } else if (this._options.content === 'attach') { - const sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(content.mimeType) || 'dat'); - if (this._options.includeTraceInfo) - content._sha1 = sha1; - else - content._file = sha1; + const shortName = calculateSha1(buffer) + '.' + (mime.getExtension(content.mimeType) || 'dat'); if (this._started) - this._delegate.onContentBlob(sha1, buffer); + content._file = this._delegate.onContentBlob(shortName, buffer); } } @@ -722,12 +710,8 @@ export class HarTracer { result.text = postData.toString(); if (content === 'attach') { - const sha1 = calculateSha1(postData) + '.' + (mime.getExtension(contentType) || 'dat'); - if (this._options.includeTraceInfo) - result._sha1 = sha1; - else - result._file = sha1; - this._delegate.onContentBlob(sha1, postData); + const shortName = calculateSha1(postData) + '.' + (mime.getExtension(contentType) || 'dat'); + result._file = this._delegate.onContentBlob(shortName, postData); } if (contentType === 'application/x-www-form-urlencoded') { @@ -778,8 +762,8 @@ function createHarEntry(pageRef: string | undefined, method: string, url: URL, f wait: -1, receive: -1 }, - _frameref: options.includeTraceInfo ? frameref : undefined, - _monotonicTime: options.includeTraceInfo ? monotonicTime() : undefined, + _frameref: frameref, + _monotonicTime: monotonicTime(), }; return harEntry; } diff --git a/packages/playwright-core/src/server/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts index c2b36758a6a94..ba32e9ed38d03 100644 --- a/packages/playwright-core/src/server/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts @@ -75,7 +75,7 @@ export async function zip(progress: Progress, stackSessions: Map, - traceSha1s: Set, + networkFiles: Set, + traceFiles: Set, recording: boolean; callsInProgress: Set; groupStack: string[]; @@ -107,7 +106,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._precreatedTracesDir = tracesDir; this._harTracer = new HarTracer(context, null, this, { content: 'attach', - includeTraceInfo: true, recordRequestOverrides: false, waitForContentOnStop: false, }); @@ -169,15 +167,20 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps tracesDir, traceFile: path.join(tracesDir, traceName + '.trace'), networkFile: path.join(tracesDir, traceName + '.network'), - resourcesDir: path.join(tracesDir, 'resources'), chunkOrdinal: 0, - traceSha1s: new Set(), - networkSha1s: new Set(), + traceFiles: new Set(), + networkFiles: new Set(), recording: false, callsInProgress: new Set(), groupStack: [], }; - this._fs.mkdir(this._state.resourcesDir); + this._fs.mkdir(path.join(tracesDir, 'resources')); + if (options.screencast) + this._fs.mkdir(path.join(tracesDir, 'screencast')); + if (options.snapshotScreen) + this._fs.mkdir(path.join(tracesDir, 'screenshots')); + if (options.snapshotAria) + this._fs.mkdir(path.join(tracesDir, 'aria')); this._fs.writeFile(this._state.networkFile, ''); // Tracing is 10x bigger if we include scripts in every trace. if (options.snapshotDom) @@ -415,11 +418,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps const entries: NameValue[] = []; entries.push({ name: 'trace.trace', value: this._state.traceFile }); entries.push({ name: 'trace.network', value: newNetworkFile }); - for (const sha1 of new Set([...this._state.traceSha1s, ...this._state.networkSha1s])) - entries.push({ name: path.join('resources', sha1), value: path.join(this._state.resourcesDir, sha1) }); + for (const file of new Set([...this._state.traceFiles, ...this._state.networkFiles])) + entries.push({ name: file, value: path.join(this._state.tracesDir, file) }); - // Only reset trace sha1s, network resources are preserved between chunks. - this._state.traceSha1s = new Set(); + // Only reset trace files, network resources are preserved between chunks. + this._state.traceFiles = new Set(); if (params.mode === 'discard') { this._isStopping = false; @@ -507,9 +510,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps const buffer = await page.screenshot(progress, { type: 'png' }).catch(() => undefined); if (!buffer || !this._state?.recording) return; - const sha1 = calculateSha1(buffer) + '.png'; - this._appendResource(sha1, buffer); - this._appendTraceEvent({ type: 'screenshot', callId: progress.metadata.id, phase, sha1 }); + const file = `screenshots/${calculateSha1(buffer)}.png`; + this._state.traceFiles.add(file); + this._appendResource(file, buffer); + this._appendTraceEvent({ type: 'screenshot', callId: progress.metadata.id, phase, file }); } private async _captureAriaSnapshot(progress: Progress, page: Page, phase: trace.ActionPhase): Promise { @@ -517,9 +521,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (!snapshot || !this._state?.recording) return; const buffer = Buffer.from(JSON.stringify(snapshot), 'utf8'); - const sha1 = calculateSha1(buffer) + '.json'; - this._appendResource(sha1, buffer); - this._appendTraceEvent({ type: 'aria-snapshot', callId: progress.metadata.id, phase, sha1 }); + const file = `aria/${calculateSha1(buffer)}.json`; + this._state.traceFiles.add(file); + this._appendResource(file, buffer); + this._appendTraceEvent({ type: 'aria-snapshot', callId: progress.metadata.id, phase, file }); } onBeforeCall(progress: Progress, sdkObject: SdkObject, parentId?: string) { @@ -586,7 +591,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps onEntryFinished(entry: har.Entry) { this._pendingHarEntries.delete(entry); const event: trace.ResourceSnapshotTraceEvent = { type: 'resource-snapshot', snapshot: entry }; - const visited = visitTraceEvent(event, this._state!.networkSha1s); + const visited = visitTraceEvent(event); this._fs.appendFile(this._state!.networkFile, JSON.stringify(visited) + '\n', true /* flush */); } @@ -594,7 +599,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps const harLines: string[] = []; for (const entry of this._pendingHarEntries) { const event: trace.ResourceSnapshotTraceEvent = { type: 'resource-snapshot', snapshot: entry }; - const visited = visitTraceEvent(event, this._state!.networkSha1s); + const visited = visitTraceEvent(event); harLines.push(JSON.stringify(visited)); } this._pendingHarEntries.clear(); @@ -602,18 +607,27 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._fs.appendFile(this._state!.networkFile, harLines.join('\n') + '\n', true /* flush */); } - onContentBlob(sha1: string, buffer: Buffer) { - this._appendResource(sha1, buffer); + onContentBlob(shortName: string, buffer: Buffer) { + const file = `resources/${shortName}`; + this._state!.networkFiles.add(file); + this._appendResource(file, buffer); + return file; } - onContentBlobAppend(sha1: string, text: string) { - if (!this._allResources.has(sha1)) - this._allResources.add(sha1); - this._fs.appendFile(path.join(this._state!.resourcesDir, sha1), text, this._state!.options.live /* flush */); + onContentBlobAppend(shortName: string, text: string) { + const file = `resources/${shortName}`; + this._state!.networkFiles.add(file); + if (!this._allResources.has(file)) + this._allResources.add(file); + this._fs.appendFile(path.join(this._state!.tracesDir, file), text, this._state!.options.live /* flush */); + return file; } - onSnapshotterBlob(blob: SnapshotterBlob): void { - this._appendResource(blob.sha1, blob.buffer); + onSnapshotterBlob(blob: SnapshotterBlob): string { + const file = `resources/${blob.sha1}`; + this._state!.traceFiles.add(file); + this._appendResource(file, blob.buffer); + return file; } onFrameSnapshot(snapshot: FrameSnapshot): void { @@ -714,42 +728,43 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps const prefix = page.guid; const onFrame = (params: types.ScreencastFrame) => { const suffix = Date.now(); - const sha1 = `${prefix}-${suffix}.jpeg`; + const file = `screencast/${prefix}-${suffix}.jpeg`; const event: trace.ScreencastFrameTraceEvent = { type: 'screencast-frame', pageId: page.guid, - sha1, + file, width: params.viewportWidth, height: params.viewportHeight, timestamp: monotonicTime(), frameSwapWallTime: params.frameSwapWallTime, }; // Make sure to write the screencast frame before adding a reference to it. - this._appendResource(sha1, params.buffer); + this._state!.traceFiles.add(file); + this._appendResource(file, params.buffer); this._appendTraceEvent(event); }; this._pageTracingRecorders.set(page, new ScreencastTracingRecorder(page.screencast, onFrame)); } private _appendTraceEvent(event: trace.TraceEvent) { - const visited = visitTraceEvent(event, this._state!.traceSha1s); + const visited = visitTraceEvent(event); // Do not flush (console) events, they are too noisy, unless we are in ui mode (live). const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log'); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush); } - private _appendResource(sha1: string, buffer: Buffer) { - if (this._allResources.has(sha1)) + private _appendResource(file: string, buffer: Buffer) { + if (this._allResources.has(file)) return; - this._allResources.add(sha1); - const resourcePath = path.join(this._state!.resourcesDir, sha1); + this._allResources.add(file); + const resourcePath = path.join(this._state!.tracesDir, file); this._fs.writeFile(resourcePath, buffer, true /* skipIfExists */); } } -function visitTraceEvent(object: any, sha1s: Set): any { +function visitTraceEvent(object: any): any { if (Array.isArray(object)) - return object.map(o => visitTraceEvent(o, sha1s)); + return object.map(o => visitTraceEvent(o)); if (object instanceof Dispatcher) return `<${(object as Dispatcher)._type}>`; if (object instanceof Buffer) @@ -758,14 +773,8 @@ function visitTraceEvent(object: any, sha1s: Set): any { return object; if (typeof object === 'object') { const result: any = {}; - for (const key in object) { - if (key === 'sha1' || key === '_sha1' || key.endsWith('Sha1')) { - const sha1 = object[key]; - if (sha1) - sha1s.add(sha1); - } - result[key] = visitTraceEvent(object[key], sha1s); - } + for (const key in object) + result[key] = visitTraceEvent(object[key]); return result; } return object; diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index c06de739ff702..275a09951f377 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -334,10 +334,12 @@ function traceDescriptor(traceDir: string, tracePrefix: string | undefined) { result.entries.push({ name, path: toFilePathUrl(path.join(traceDir, name)) }); } - const resourcesDir = path.join(traceDir, 'resources'); - if (fs.existsSync(resourcesDir)) { - for (const name of fs.readdirSync(resourcesDir)) - result.entries.push({ name: 'resources/' + name, path: toFilePathUrl(path.join(resourcesDir, name)) }); + for (const dir of ['resources', 'screencast', 'screenshots', 'aria', 'attachments', 'src']) { + const dirPath = path.join(traceDir, dir); + if (fs.existsSync(dirPath)) { + for (const name of fs.readdirSync(dirPath)) + result.entries.push({ name: dir + '/' + name, path: toFilePathUrl(path.join(dirPath, name)) }); + } } return result; } diff --git a/packages/playwright-core/src/tools/trace/traceAttachments.ts b/packages/playwright-core/src/tools/trace/traceAttachments.ts index 5c8b51de95aef..186748461d28b 100644 --- a/packages/playwright-core/src/tools/trace/traceAttachments.ts +++ b/packages/playwright-core/src/tools/trace/traceAttachments.ts @@ -49,8 +49,8 @@ export async function traceAttachment(attachmentId: string, options: { output?: } let content: Buffer | undefined; - if (attachment.sha1) { - const blob = await trace.loader.resourceForSha1(attachment.sha1); + if (attachment.file) { + const blob = await trace.loader.resourceEntry(attachment.file); if (blob) content = Buffer.from(await blob.arrayBuffer()); } else if (attachment.base64) { diff --git a/packages/playwright-core/src/tools/trace/traceRequests.ts b/packages/playwright-core/src/tools/trace/traceRequests.ts index 547669d8b92ab..fff0046a18975 100644 --- a/packages/playwright-core/src/tools/trace/traceRequests.ts +++ b/packages/playwright-core/src/tools/trace/traceRequests.ts @@ -116,9 +116,9 @@ export async function traceRequest(requestId: string) { // Request body if (r.request.postData) { console.log('\n Request body'); - const resource = r.request.postData._sha1 ?? r.request.postData._file; + const resource = r.request.postData._file; if (resource) { - console.log(` ${path.relative(process.cwd(), path.join(trace.model.traceUri, 'resources', resource))}`); + console.log(` ${path.relative(process.cwd(), path.join(trace.model.traceUri, resource))}`); } else { const text = r.request.postData.text.length > 2000 ? r.request.postData.text.substring(0, 2000) + '...' @@ -136,10 +136,10 @@ export async function traceRequest(requestId: string) { // Response body if (r.response.bodySize > 0) { - const resource = r.response.content._sha1 ?? r.response.content._file; + const resource = r.response.content._file; if (resource) { console.log('\n Response body'); - console.log(` ${path.relative(process.cwd(), path.join(trace.model.traceUri, 'resources', resource))}`); + console.log(` ${path.relative(process.cwd(), path.join(trace.model.traceUri, resource))}`); } else if (r.response.content.text) { const text = r.response.content.text.length > 2000 ? r.response.content.text.substring(0, 2000) + '...' diff --git a/packages/playwright-core/src/tools/trace/traceScreenshot.ts b/packages/playwright-core/src/tools/trace/traceScreenshot.ts index 0d7a9a62ba0f7..4082f17e53f06 100644 --- a/packages/playwright-core/src/tools/trace/traceScreenshot.ts +++ b/packages/playwright-core/src/tools/trace/traceScreenshot.ts @@ -38,21 +38,21 @@ export async function traceScreenshot(actionId: string, options: { output?: stri const callId = action.callId; const storage = trace.loader.storage(); const snapshotNames = ['input', 'before', 'after']; - let sha1: string | undefined; + let file: string | undefined; for (const name of snapshotNames) { const renderer = storage.snapshotByName(pageId, `${name}@${callId}`); - sha1 = renderer?.closestScreenshot(); - if (sha1) + file = renderer?.closestScreenshot(); + if (file) break; } - if (!sha1) { + if (!file) { console.error(`No screenshot found for action '${actionId}'.`); process.exitCode = 1; return; } - const blob = await trace.loader.resourceForSha1(sha1); + const blob = await trace.loader.resourceEntry(file); if (!blob) { console.error(`Screenshot resource not found.`); process.exitCode = 1; diff --git a/packages/playwright-core/src/tools/trace/traceSnapshot.ts b/packages/playwright-core/src/tools/trace/traceSnapshot.ts index 7641eddead943..b4f003f56f26c 100644 --- a/packages/playwright-core/src/tools/trace/traceSnapshot.ts +++ b/packages/playwright-core/src/tools/trace/traceSnapshot.ts @@ -84,7 +84,7 @@ export async function traceSnapshot(actionId: string, options: { name?: string, } async function serveTraceSnapshot(storage: SnapshotStorage, loader: TraceLoader, pageId: string, snapshotKey: string): Promise<{ url: string, stop: () => Promise }> { - const snapshotServer = new SnapshotServer(storage, sha1 => loader.resourceForSha1(sha1)); + const snapshotServer = new SnapshotServer(storage, file => loader.resourceEntry(file)); const httpServer = new HttpServer(); httpServer.routePrefix('/snapshot/', (request: any, response: any) => { diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index 3de5f843f6655..5eb9aac3be810 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -206,7 +206,7 @@ export class TestTracing { } for (const sourceFile of sourceFiles) { await fs.promises.readFile(sourceFile, 'utf8').then(source => { - zipFile.addBuffer(Buffer.from(source), 'resources/src@' + calculateSha1(sourceFile) + '.txt'); + zipFile.addBuffer(Buffer.from(source), 'src/' + calculateSha1(sourceFile) + path.extname(sourceFile)); }).catch(() => {}); } } @@ -225,13 +225,13 @@ export class TestTracing { continue; const sha1 = calculateSha1(content); - attachment.sha1 = sha1; + attachment.file = 'attachments/' + sha1; delete attachment.path; delete attachment.base64; if (sha1s.has(sha1)) continue; sha1s.add(sha1); - zipFile.addBuffer(content, 'resources/' + sha1); + zipFile.addBuffer(content, attachment.file); } } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 15a5f23d3d8c8..79d1bc4911923 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -128,7 +128,7 @@ async function innerLoadTrace(traceUri: string, progress: Progress): Promise traceLoader.resourceForSha1(sha1)); + const snapshotServer = new SnapshotServer(traceLoader.storage(), file => traceLoader.resourceEntry(file)); return { traceLoader, snapshotServer }; } @@ -155,8 +155,8 @@ async function doFetch(event: FetchEvent): Promise { const isNavigation = !!event.resultingClientId; const client = event.clientId ? await self.clients.get(event.clientId) : undefined; - if (isNavigation && !relativePath?.startsWith('/sha1/')) { - // Navigation request. Download is a /sha1/ navigation, ignore them here. + if (isNavigation && !relativePath?.startsWith('/file/')) { + // Navigation request. Download is a /file/ navigation, ignore them here. // Snapshot iframe navigation request. if (relativePath?.startsWith('/snapshot/')) { @@ -196,7 +196,7 @@ async function doFetch(event: FetchEvent): Promise { } // These commands all require a loaded trace. - if (relativePath === '/contexts' || relativePath.startsWith('/snapshotInfo/') || relativePath.startsWith('/closest-screenshot/') || relativePath.startsWith('/sha1/')) { + if (relativePath === '/contexts' || relativePath.startsWith('/snapshotInfo/') || relativePath.startsWith('/closest-screenshot/') || relativePath.startsWith('/file/')) { if (!client) return new Response('Sub-resource without a client', { status: 500 }); @@ -221,8 +221,8 @@ async function doFetch(event: FetchEvent): Promise { return loadedTrace!.snapshotServer.serveClosestScreenshot(pageOrFrameId, url.searchParams); } - if (relativePath.startsWith('/sha1/')) { - const blob = await loadedTrace!.traceLoader.resourceForSha1(relativePath.slice('/sha1/'.length)); + if (relativePath.startsWith('/file/')) { + const blob = await loadedTrace!.traceLoader.resourceEntry(relativePath.slice('/file/'.length)); if (blob) return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); return new Response(null, { status: 404 }); diff --git a/packages/trace-viewer/src/third_party/devtools.ts b/packages/trace-viewer/src/third_party/devtools.ts index d4b935905b7c2..f355a327e67e4 100644 --- a/packages/trace-viewer/src/third_party/devtools.ts +++ b/packages/trace-viewer/src/third_party/devtools.ts @@ -290,7 +290,7 @@ export async function generateFetchCall(model: TraceModel | undefined, resource: } async function fetchRequestPostData(model: TraceModel | undefined, resource: Entry) { - return (model && resource.request.postData?._sha1) ? - await fetch(model.createRelativeUrl(`sha1/${resource.request.postData._sha1}`)).then(r => r.text()) + return (model && resource.request.postData?._file) ? + await fetch(model.createRelativeUrl(`file/${resource.request.postData._file}`)).then(r => r.text()) : resource.request.postData?.text; } diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index f7bdd6bc18c57..ede74609510c3 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -41,7 +41,7 @@ const ExpandableAttachment: React.FunctionComponent = const ref = React.useRef(null); const isTextAttachment = isTextualMimeType(attachment.contentType); - const hasContent = !!attachment.sha1 || !!attachment.path; + const hasContent = !!attachment.file || !!attachment.path; React.useEffect(() => { if (reveal) { @@ -102,7 +102,7 @@ export const AttachmentsTab: React.FunctionComponent<{ const diffMap = new Map(); for (const attachment of attachments) { - if (!attachment.path && !attachment.sha1) + if (!attachment.path && !attachment.file) continue; const match = attachment.name.match(/^(.*)-(expected|actual|diff)\.png$/); if (match) { @@ -156,8 +156,8 @@ export const AttachmentsTab: React.FunctionComponent<{ }; export function attachmentURL(model: TraceModel | undefined, attachment: Attachment) { - if (model && attachment.sha1) - return model.createRelativeUrl(`sha1/${attachment.sha1}`) ; + if (model && attachment.file) + return model.createRelativeUrl(`file/${attachment.file}`) ; return `file?path=${encodeURIComponent(attachment.path!)}`; } @@ -169,5 +169,5 @@ function downloadURL(model: TraceModel | undefined, attachment: Attachment) { } function attachmentKey(attachment: Attachment, index: number) { - return index + '-' + (attachment.sha1 ? `sha1-` + attachment.sha1 : `path-` + attachment.path); + return index + '-' + (attachment.file ? `file-` + attachment.file : `path-` + attachment.path); } diff --git a/packages/trace-viewer/src/ui/filmStrip.tsx b/packages/trace-viewer/src/ui/filmStrip.tsx index 4fa56cfddc4c3..93ccc7df180f6 100644 --- a/packages/trace-viewer/src/ui/filmStrip.tsx +++ b/packages/trace-viewer/src/ui/filmStrip.tsx @@ -77,7 +77,7 @@ export const FilmStrip: React.FunctionComponent<{ left: Math.min(previewPoint!.x, measure.width - (previewSize ? previewSize.width : 0) - 10), }}> {previewImage && previewSize &&
- +
} {previewPoint.action &&
{renderAction(previewPoint.action, previewPoint)}
} @@ -115,7 +115,7 @@ const FilmStripLane: React.FunctionComponent<{ frames.push(
q.name.toLowerCase() === 'content-type'); const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; - if (resource.request.postData._sha1) { - const response = await fetch(model.createRelativeUrl(`sha1/${resource.request.postData._sha1}`)); + if (resource.request.postData._file) { + const response = await fetch(model.createRelativeUrl(`file/${resource.request.postData._file}`)); return { text: await response.text(), mimeType: requestContentType }; } else { return { text: resource.request.postData.text, mimeType: requestContentType }; @@ -216,10 +216,10 @@ const ResponseTab: React.FunctionComponent<{ React.useEffect(() => { const readResources = async () => { - if (model && resource.response.content._sha1) { + if (model && resource.response.content._file) { const useBase64 = resource.response.content.mimeType.includes('image'); const isFont = resource.response.content.mimeType.includes('font'); - const response = await fetch(model.createRelativeUrl(`sha1/${resource.response.content._sha1}`)); + const response = await fetch(model.createRelativeUrl(`file/${resource.response.content._file}`)); if (useBase64) { const blob = await response.blob(); const reader = new FileReader(); @@ -244,7 +244,7 @@ const ResponseTab: React.FunctionComponent<{ const formatResult = useFormattedBody(responseBody, showFormattedResponse); return
- {!resource.response.content._sha1 &&
Response body is not available for this request.
} + {!resource.response.content._file &&
Response body is not available for this request.
} {responseBody && responseBody.font && } {responseBody && responseBody.dataUrl &&
} {responseBody && responseBody.text !== undefined &&
@@ -326,9 +326,9 @@ const WebSocketMessagesTab: React.FunctionComponent<{ const indexedMessages = useAsyncMemo(async () => { if (resource._webSocketMessages) return resource._webSocketMessages.map((m, index) => ({ ...m, index, byteLength: messageByteLength(m) })); - if (model && resource.response.content._sha1) { + if (model && resource.response.content._file) { try { - const response = await fetch(model.createRelativeUrl(`sha1/${resource.response.content._sha1}`)); + const response = await fetch(model.createRelativeUrl(`file/${resource.response.content._file}`)); if (!response.ok) return []; const text = await response.text(); diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx index 0221963c8ddd1..046c9763f6864 100644 --- a/packages/trace-viewer/src/ui/sourceTab.tsx +++ b/packages/trace-viewer/src/ui/sourceTab.tsx @@ -28,6 +28,12 @@ import { ToolbarButton } from '@web/components/toolbarButton'; import { Toolbar } from '@web/components/toolbar'; import { useTraceModel } from './traceModelContext'; +function extname(file: string): string { + const basename = file.substring(Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\')) + 1); + const dot = basename.lastIndexOf('.'); + return dot <= 0 ? '' : basename.substring(dot); +} + function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sources: Map, rootDir?: string, fallbackLocation?: SourceLocation) { const model = useTraceModel(); return useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => { @@ -55,7 +61,11 @@ function useSources(stack: StackFrame[] | undefined, selectedFrame: number, sour } else if (source.content === undefined || (location === fallbackLocation)) { const sha1 = await calculateSha1(file); try { - let response = model ? await fetch(model.createRelativeUrl(`sha1/src@${sha1}.txt`)) : undefined; + let response = model ? await fetch(model.createRelativeUrl(`file/src/${sha1}${extname(file)}`)) : undefined; + if (!response || response.status === 404) { + // Older traces stored sources under resources/src@.txt. + response = model ? await fetch(model.createRelativeUrl(`file/resources/src@${sha1}.txt`)) : undefined; + } if (!response || response.status === 404) response = await fetch(`file?path=${encodeURIComponent(file)}`); if (response.status >= 400) diff --git a/packages/trace/src/har.ts b/packages/trace/src/har.ts index 63d81852af9ff..3711f033d55a2 100644 --- a/packages/trace/src/har.ts +++ b/packages/trace/src/har.ts @@ -140,7 +140,6 @@ export type PostData = { params: Param[]; text: string; comment?: string; - _sha1?: string; _file?: string; }; @@ -159,7 +158,6 @@ export type Content = { text?: string; encoding?: string; comment?: string; - _sha1?: string; _file?: string; }; diff --git a/packages/trace/src/snapshot.ts b/packages/trace/src/snapshot.ts index 43d002f616860..06334a2b78a50 100644 --- a/packages/trace/src/snapshot.ts +++ b/packages/trace/src/snapshot.ts @@ -33,7 +33,7 @@ export type NodeSnapshot = export type ResourceOverride = { url: string, - sha1?: string, + file?: string, ref?: number }; diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts index a7d6ba58d6d49..bfba3d39233dc 100644 --- a/packages/trace/src/trace.ts +++ b/packages/trace/src/trace.ts @@ -100,7 +100,7 @@ export type ContextCreatedTraceEvent = { export type ScreencastFrameTraceEvent = { type: 'screencast-frame', pageId: string, - sha1: string, + file: string, width: number, height: number, timestamp: number, @@ -113,14 +113,14 @@ export type ScreenshotTraceEvent = { type: 'screenshot', callId: string, phase: ActionPhase, - sha1: string, + file: string, }; export type AriaSnapshotTraceEvent = { type: 'aria-snapshot', callId: string, phase: ActionPhase, - sha1: string, + file: string, }; export type BeforeActionTraceEvent = { @@ -150,7 +150,7 @@ export type AfterActionTraceEventAttachment = { name: string; contentType: string; path?: string; - sha1?: string; + file?: string; base64?: string; }; diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 7fefdbfeffe6e..15e4da5883d10 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -728,7 +728,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { await browser.close(); const { resources } = await parseTraceRaw(testInfo.outputPath('trace1.zip')); - const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt')); + const sourceNames = Array.from(resources.keys()).filter(k => k.startsWith('src/')); expect(sourceNames.length).toBe(1); const sourceFile = resources.get(sourceNames[0]); const thisFile = await fs.promises.readFile(__filename); diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index 143a3186b1dc0..4e5640d797fe5 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -1150,9 +1150,11 @@ it.describe('tracing.startHar', () => { const log = JSON.parse(fs.readFileSync(harPath).toString()).log as Log; const styleEntry = log.entries.find(e => e.request.url.endsWith('/one-style.css'))!; - const sha1 = (styleEntry.response.content as any)._file as string; - expect(sha1).toBeTruthy(); - const resourcePath = path.join(resourcesDir, sha1); + const file = (styleEntry.response.content as any)._file as string; + expect(file).toBeTruthy(); + // _file is relative to the har file directory. + const resourcePath = path.join(path.dirname(harPath), file); + expect(resourcePath.startsWith(resourcesDir + path.sep)).toBe(true); expect(fs.existsSync(resourcePath)).toBe(true); expect(fs.readFileSync(resourcePath).toString()).toContain('pink'); }); diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 85466e461ff1c..1cccd3e0d0bcb 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -58,10 +58,10 @@ test('should collect trace with resources, but no js', async ({ context, page, s expect(events.some(e => e.type === 'screencast-frame')).toBeTruthy(); const style = events.find(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css')); expect(style).toBeTruthy(); - expect(style.snapshot.response.content._sha1).toBeTruthy(); + expect(style.snapshot.response.content._file).toBeTruthy(); const script = events.find(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('script.js')); expect(script).toBeTruthy(); - expect(script.snapshot.response.content._sha1).toBe(undefined); + expect(script.snapshot.response.content._file).toBe(undefined); }); test('should use the correct title for event driven callbacks', async ({ context, page, server }, testInfo) => { @@ -130,7 +130,8 @@ test('should collect action screenshots', async ({ context, page, server }, test const screenshots = events.filter(e => e.type === 'screenshot' && e.callId === clickCallId); expect(screenshots.map(e => e.phase)).toEqual(['before', 'action', 'after']); for (const screenshot of screenshots) { - const buffer = resources.get('resources/' + screenshot.sha1); + expect(screenshot.file).toMatch(/^screenshots\/[0-9a-f]{40}\.png$/); + const buffer = resources.get(screenshot.file); expect(PNG.sync.read(buffer).width).toBeGreaterThan(0); } }); @@ -147,8 +148,8 @@ test('should collect aria snapshots', async ({ context, page, server }, testInfo expect(ariaSnapshots.map(e => e.phase)).toEqual(['before', 'action', 'after']); const hasButton = nodes => nodes.some(node => typeof node === 'object' && (node.role === 'button' && node.name === 'Click target' || hasButton(node.children ?? []))); for (const ariaSnapshot of ariaSnapshots) { - expect(ariaSnapshot.sha1).toMatch(/\.json$/); - const snapshot = JSON.parse(resources.get('resources/' + ariaSnapshot.sha1).toString()); + expect(ariaSnapshot.file).toMatch(/^aria\/[0-9a-f]{40}\.json$/); + const snapshot = JSON.parse(resources.get(ariaSnapshot.file).toString()); expect(hasButton(snapshot)).toBe(true); } }); @@ -336,7 +337,7 @@ test('should not include trace resources from the previous chunks', async ({ con expect(names.filter(n => n.endsWith('.html')).length).toBe(1); jpegs = names.filter(n => n.endsWith('.jpeg')); // 1 source file for the test. - expect(names.filter(n => n.endsWith('.txt')).length).toBe(1); + expect(names.filter(n => n.startsWith('src/')).length).toBe(1); } { @@ -347,7 +348,7 @@ test('should not include trace resources from the previous chunks', async ({ con // screenshots from the previous chunk should not be preserved. expect(names.filter(n => jpegs.includes(n)).length).toBe(0); // 0 source files for the second test. - expect(names.filter(n => n.endsWith('.txt')).length).toBe(0); + expect(names.filter(n => n.startsWith('src/')).length).toBe(0); } }); @@ -382,8 +383,9 @@ test('should collect sources', async ({ context, page, server }, testInfo) => { await context.tracing.stop({ path: testInfo.outputPath('trace1.zip') }); const { resources } = await parseTraceRaw(testInfo.outputPath('trace1.zip')); - const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt')); + const sourceNames = Array.from(resources.keys()).filter(k => k.startsWith('src/')); expect(sourceNames.length).toBe(1); + expect(sourceNames[0]).toMatch(/^src\/[0-9a-f]{40}\.ts$/); const sourceFile = resources.get(sourceNames[0]); const thisFile = await fs.promises.readFile(__filename); expect(sourceFile).toEqual(thisFile); @@ -502,14 +504,14 @@ for (const params of [ for (const frame of frames) { expect.soft(frame.width).toBe(params.width); expect.soft(frame.height).toBe(params.height); - const buffer = resources.get('resources/' + frame.sha1); + const buffer = resources.get(frame.file); const image = jpegjs.decode(buffer); expect.soft(image.width).toBe(previewWidth); expect.soft(image.height).toBe(previewHeight); } const frame = frames[frames.length - 1]; // pick last frame. - const buffer = resources.get('resources/' + frame.sha1); + const buffer = resources.get(frame.file); const image = jpegjs.decode(buffer); expect(image.data.byteLength).toBe(previewWidth * previewHeight * 4); expectRed(image.data, previewWidth * previewHeight * 4 / 2 + previewWidth * 4 / 2); // center is red @@ -751,7 +753,7 @@ test('should store postData for global request', async ({ request, server }, tes const actions = trace.events.filter(e => e.type === 'resource-snapshot'); expect(actions).toHaveLength(1); const req = actions[0].snapshot.request; - expect(req.postData?._sha1).toBeTruthy(); + expect(req.postData?._file).toBeTruthy(); expect(req).toEqual(expect.objectContaining({ method: 'POST', url diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index df431af1e858a..82ce623a12e86 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -761,7 +761,7 @@ it.describe('screencast', () => { const { events, resources } = await parseTraceRaw(traceFile); const frame = events.filter(e => e.type === 'screencast-frame').pop(); - const buffer = resources.get('resources/' + frame.sha1); + const buffer = resources.get(frame.file); const image = jpegjs.decode(buffer); expect(image.width).toBe(size.width); expect(image.height).toBe(size.height); diff --git a/tests/mcp/tracing.spec.ts b/tests/mcp/tracing.spec.ts index 9f9e0edbdefb6..8aa4f23e1aaa9 100644 --- a/tests/mcp/tracing.spec.ts +++ b/tests/mcp/tracing.spec.ts @@ -45,6 +45,7 @@ test('check that trace is saved with browser_start_tracing', async ({ startClien const files = await fs.promises.readdir(path.join(outputDir, 'traces')); expect(files).toEqual([ 'resources', + 'screencast', expect.stringMatching(/trace-\d+\.network/), expect.stringMatching(/trace-\d+\.stacks/), expect.stringMatching(/trace-\d+\.trace/), @@ -78,6 +79,7 @@ test('check that trace is saved with browser_start_tracing (no output dir)', asy const files = await fs.promises.readdir(testInfo.outputPath('.playwright-mcp', 'traces')); expect(files).toEqual([ 'resources', + 'screencast', expect.stringMatching(/trace-\d+\.network/), expect.stringMatching(/trace-\d+\.stacks/), expect.stringMatching(/trace-\d+\.trace/), diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index 810be371d828a..81329f1416448 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -226,7 +226,7 @@ test('should save sources when requested', async ({ runInlineTest }, testInfo) = }, { workers: 1 }); expect(result.exitCode).toEqual(0); const { resources } = await parseTraceRaw(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); - expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(1); + expect([...resources.keys()].filter(name => name.startsWith('src/'))).toHaveLength(1); }); test('should not save sources when not requested', async ({ runInlineTest }, testInfo) => { @@ -250,7 +250,7 @@ test('should not save sources when not requested', async ({ runInlineTest }, tes }, { workers: 1 }); expect(result.exitCode).toEqual(0); const { resources } = await parseTraceRaw(testInfo.outputPath('test-results', 'a-pass', 'trace.zip')); - expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(0); + expect([...resources.keys()].filter(name => name.startsWith('src/'))).toHaveLength(0); }); test('should work in serial mode', async ({ runInlineTest }, testInfo) => { @@ -522,10 +522,10 @@ test('should include attachments by default', async ({ runInlineTest, server }, expect(trace.model.actions[1].attachments).toEqual([{ name: 'foo', contentType: 'text/plain', - sha1: expect.any(String), + file: expect.any(String), }]); const { resources } = await parseTraceRaw(tracePath); - expect([...resources.keys()]).toContain(`resources/${trace.model.actions[1].attachments[0].sha1}`); + expect([...resources.keys()]).toContain(trace.model.actions[1].attachments[0].file); }); test('should opt out of attachments', async ({ runInlineTest, server }, testInfo) => { @@ -553,7 +553,7 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo ]); expect(trace.model.actions[1].attachments).toEqual(undefined); const { resources } = await parseTraceRaw(tracePath); - expect([...resources.keys()].filter(f => f.startsWith('resources/') && !f.startsWith('resources/src@'))).toHaveLength(0); + expect([...resources.keys()].filter(f => f.startsWith('attachments/') || f.startsWith('resources/'))).toHaveLength(0); }); test('should record with custom page fixture', async ({ runInlineTest }, testInfo) => {