From f7420c651cfa0b0bd10ea206646eda539d35649c Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 7 Aug 2024 13:43:16 +0200 Subject: [PATCH] store screenshots in zip file --- .../src/server/trace/recorder/snapshotter.ts | 8 +++++++ .../trace/recorder/snapshotterInjected.ts | 22 +++++++++++++++--- packages/trace-viewer/src/snapshotRenderer.ts | 8 ++++++- packages/trace-viewer/src/sw.ts | 2 +- packages/trace-viewer/src/traceModel.ts | 4 ++-- ...-with-preserveDrawingBuffer-1-chromium.png | Bin 4289 -> 8928 bytes 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts index d3b7705a86..cfe4f14725 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts @@ -151,6 +151,14 @@ export class Snapshotter { snapshot.resourceOverrides.push({ url, ref: content }); } } + + for (const [sha1, contents] of Object.entries(data.canvasRenderResults)) { + this._delegate.onSnapshotterBlob({ + sha1, buffer: Buffer.from(contents, 'base64') + }); + snapshot.resourceOverrides.push({ url: 'TODO: this is required but unused', sha1 }); + } + this._delegate.onFrameSnapshot(snapshot); }); await Promise.all(snapshots); diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts index 0354a0394b..057435a728 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts @@ -29,6 +29,7 @@ export type SnapshotData = { url: string, timestamp: number, collectionTime: number, + canvasRenderResults: Record, }; export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: boolean) { @@ -86,6 +87,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: private _readingStyleSheet = false; // To avoid invalidating due to our own reads. private _fakeBase: HTMLBaseElement; private _observer: MutationObserver; + private _capturedCanvases = new Set(); constructor() { const invalidateCSSGroupingRule = (rule: CSSGroupingRule) => { @@ -319,14 +321,15 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: this._handleMutations(this._observer.takeRecords()); const definedCustomElements = new Set(); + const canvasRenderResults: Record = {}; const visitNode = (node: Node | ShadowRoot): { equals: boolean, n: NodeSnapshot } | undefined => { const nodeType = node.nodeType; const nodeName = nodeType === Node.DOCUMENT_FRAGMENT_NODE ? 'template' : node.nodeName; if (nodeType !== Node.ELEMENT_NODE && - nodeType !== Node.DOCUMENT_FRAGMENT_NODE && - nodeType !== Node.TEXT_NODE) + nodeType !== Node.DOCUMENT_FRAGMENT_NODE && + nodeType !== Node.TEXT_NODE) return; if (nodeName === 'SCRIPT') return; @@ -406,7 +409,19 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: if (nodeName === 'CANVAS') { const canvas = node as HTMLCanvasElement; - attrs['__playwright_canvas_'] = canvas.toDataURL('image/webp', 1); + const requestedMIME = 'image/webp'; + const dataURL = canvas.toDataURL(requestedMIME); + const actualMIME = dataURL.substring('data:'.length, dataURL.indexOf(';')); + const contentsB64 = dataURL.substring(dataURL.indexOf(',') + 1); + const sha = '' + contentsB64.length; // TODO + + attrs['__playwright_canvas_sha_'] = sha; + attrs['__playwright_canvas_mime_'] = actualMIME; + + if (!this._capturedCanvases.has(sha)) { + this._capturedCanvases.add(sha); + canvasRenderResults[sha] = contentsB64; + } } if (nodeType === Node.DOCUMENT_FRAGMENT_NODE) @@ -579,6 +594,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: url: location.href, timestamp, collectionTime: 0, + canvasRenderResults, }; for (const sheet of this._staleStyleSheets) { diff --git a/packages/trace-viewer/src/snapshotRenderer.ts b/packages/trace-viewer/src/snapshotRenderer.ts index 87cb52b655..d3dd95ebde 100644 --- a/packages/trace-viewer/src/snapshotRenderer.ts +++ b/packages/trace-viewer/src/snapshotRenderer.ts @@ -285,7 +285,13 @@ function snapshotScript(...targetIds: (string | undefined)[]) { const context = canvas.getContext('2d'); context?.drawImage(img, 0, 0); }; - img.src = canvas.getAttribute('__playwright_canvas_')!; + const url = new URL(window.location.href); + const index = url.pathname.lastIndexOf('/snapshot/'); + if (index !== -1) + url.pathname = url.pathname.substring(0, index + 1); + url.pathname += `sha1/${canvas.getAttribute('__playwright_canvas_sha_')}`; + url.searchParams.set('ct', canvas.getAttribute('__playwright_canvas_mime_')!); + img.src = url.toString(); } { diff --git a/packages/trace-viewer/src/sw.ts b/packages/trace-viewer/src/sw.ts index 7888aa6a30..6c06a88488 100644 --- a/packages/trace-viewer/src/sw.ts +++ b/packages/trace-viewer/src/sw.ts @@ -133,7 +133,7 @@ async function doFetch(event: FetchEvent): Promise { // Sha1 for sources is based on the file path, can't load it of a random model. const sha1 = relativePath.slice('/sha1/'.length); for (const trace of loadedTraces.values()) { - const blob = await trace.traceModel.resourceForSha1(sha1); + const blob = await trace.traceModel.resourceForSha1(sha1, url.searchParams.get('ct')); if (blob) return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); } diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 0fc1a73efa..c0cae40897 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -112,11 +112,11 @@ export class TraceModel { return this._backend.hasEntry(filename); } - async resourceForSha1(sha1: string): Promise { + async resourceForSha1(sha1: string, contentTypeOverride?: string | null): Promise { const blob = await this._backend.readBlob('resources/' + sha1); if (!blob) return; - return new Blob([blob], { type: this._resourceToContentType.get(sha1) || 'application/octet-stream' }); + return new Blob([blob], { type: contentTypeOverride || this._resourceToContentType.get(sha1) || 'application/octet-stream' }); } storage(): SnapshotStorage { diff --git a/tests/library/trace-viewer.spec.ts-snapshots/should-show-canvas-webgl-works-with-preserveDrawingBuffer-1-chromium.png b/tests/library/trace-viewer.spec.ts-snapshots/should-show-canvas-webgl-works-with-preserveDrawingBuffer-1-chromium.png index c3f2b66b2b5c870252a51af9a06b5f65808dd0b1..c3f6361bdec7aec0d2253ad3665ba4cc14709bcc 100644 GIT binary patch literal 8928 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 literal 4289 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 AQvd(}