store screenshots in zip file

This commit is contained in:
Simon Knott 2024-08-07 13:43:16 +02:00
parent 651f5a3ea4
commit f7420c651c
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
6 changed files with 37 additions and 7 deletions

View file

@ -151,6 +151,14 @@ export class Snapshotter {
snapshot.resourceOverrides.push({ url, ref: content }); 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); this._delegate.onFrameSnapshot(snapshot);
}); });
await Promise.all(snapshots); await Promise.all(snapshots);

View file

@ -29,6 +29,7 @@ export type SnapshotData = {
url: string, url: string,
timestamp: number, timestamp: number,
collectionTime: number, collectionTime: number,
canvasRenderResults: Record<string, string>,
}; };
export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript: boolean) { 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 _readingStyleSheet = false; // To avoid invalidating due to our own reads.
private _fakeBase: HTMLBaseElement; private _fakeBase: HTMLBaseElement;
private _observer: MutationObserver; private _observer: MutationObserver;
private _capturedCanvases = new Set<string>();
constructor() { constructor() {
const invalidateCSSGroupingRule = (rule: CSSGroupingRule) => { const invalidateCSSGroupingRule = (rule: CSSGroupingRule) => {
@ -319,6 +321,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
this._handleMutations(this._observer.takeRecords()); this._handleMutations(this._observer.takeRecords());
const definedCustomElements = new Set<string>(); const definedCustomElements = new Set<string>();
const canvasRenderResults: Record<string, string> = {};
const visitNode = (node: Node | ShadowRoot): { equals: boolean, n: NodeSnapshot } | undefined => { const visitNode = (node: Node | ShadowRoot): { equals: boolean, n: NodeSnapshot } | undefined => {
const nodeType = node.nodeType; const nodeType = node.nodeType;
@ -406,7 +409,19 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
if (nodeName === 'CANVAS') { if (nodeName === 'CANVAS') {
const canvas = node as HTMLCanvasElement; 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) if (nodeType === Node.DOCUMENT_FRAGMENT_NODE)
@ -579,6 +594,7 @@ export function frameSnapshotStreamer(snapshotStreamer: string, removeNoScript:
url: location.href, url: location.href,
timestamp, timestamp,
collectionTime: 0, collectionTime: 0,
canvasRenderResults,
}; };
for (const sheet of this._staleStyleSheets) { for (const sheet of this._staleStyleSheets) {

View file

@ -285,7 +285,13 @@ function snapshotScript(...targetIds: (string | undefined)[]) {
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
context?.drawImage(img, 0, 0); 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();
} }
{ {

View file

@ -133,7 +133,7 @@ async function doFetch(event: FetchEvent): Promise<Response> {
// Sha1 for sources is based on the file path, can't load it of a random model. // Sha1 for sources is based on the file path, can't load it of a random model.
const sha1 = relativePath.slice('/sha1/'.length); const sha1 = relativePath.slice('/sha1/'.length);
for (const trace of loadedTraces.values()) { 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) if (blob)
return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) }); return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) });
} }

View file

@ -112,11 +112,11 @@ export class TraceModel {
return this._backend.hasEntry(filename); return this._backend.hasEntry(filename);
} }
async resourceForSha1(sha1: string): Promise<Blob | undefined> { async resourceForSha1(sha1: string, contentTypeOverride?: string | null): Promise<Blob | undefined> {
const blob = await this._backend.readBlob('resources/' + sha1); const blob = await this._backend.readBlob('resources/' + sha1);
if (!blob) if (!blob)
return; 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 { storage(): SnapshotStorage {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB