diff --git a/src/server/snapshot/snapshotRenderer.ts b/src/server/snapshot/snapshotRenderer.ts index 8979745740..1d80ff6235 100644 --- a/src/server/snapshot/snapshotRenderer.ts +++ b/src/server/snapshot/snapshotRenderer.ts @@ -21,11 +21,13 @@ export class SnapshotRenderer { private _index: number; readonly snapshotName: string | undefined; private _resources: ResourceSnapshot[]; + private _snapshot: FrameSnapshot; constructor(resources: ResourceSnapshot[], snapshots: FrameSnapshot[], index: number) { this._resources = resources; this._snapshots = snapshots; this._index = index; + this._snapshot = snapshots[index]; this.snapshotName = snapshots[index].snapshotName; } @@ -73,10 +75,10 @@ export class SnapshotRenderer { return (n as any)._string; }; - const snapshot = this._snapshots[this._index]; + const snapshot = this._snapshot; let html = visit(snapshot.html, this._index); if (!html) - return { html: '', resources: {} }; + return { html: '', pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; if (snapshot.doctype) html = `` + html; @@ -85,26 +87,46 @@ export class SnapshotRenderer { `; - const resources: { [key: string]: { resourceId: string, sha1?: string } } = {}; - // First capture all resources for all frames, to account for memory cache. - for (const resource of this._resources) { - if (resource.timestamp >= snapshot.timestamp) - break; - resources[resource.url] = { resourceId: resource.resourceId }; - } - // Then overwrite with the ones from our frame. + return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; + } + + resourceByUrl(url: string): ResourceSnapshot | undefined { + const snapshot = this._snapshot; + let result: ResourceSnapshot | undefined; + + // First try locating exact resource belonging to this frame. for (const resource of this._resources) { if (resource.timestamp >= snapshot.timestamp) break; if (resource.frameId !== snapshot.frameId) continue; - resources[resource.url] = { resourceId: resource.resourceId }; + if (resource.url === url) { + result = resource; + break; + } } - for (const o of snapshot.resourceOverrides) { - const resource = resources[o.url]; - resource.sha1 = o.sha1; + + if (!result) { + // Then fall back to resource with this URL to account for memory cache. + for (const resource of this._resources) { + if (resource.timestamp >= snapshot.timestamp) + break; + if (resource.url === url) + return resource; + } } - return { html, resources }; + + if (result) { + // Patch override if necessary. + for (const o of snapshot.resourceOverrides) { + if (url === o.url && o.sha1) { + result = { ...result, responseSha1: o.sha1 }; + break; + } + } + } + + return result; } } diff --git a/src/server/snapshot/snapshotServer.ts b/src/server/snapshot/snapshotServer.ts index 8982405025..9ad97df8e3 100644 --- a/src/server/snapshot/snapshotServer.ts +++ b/src/server/snapshot/snapshotServer.ts @@ -62,7 +62,7 @@ export class SnapshotServer { private _serveServiceWorker(request: http.IncomingMessage, response: http.ServerResponse): boolean { function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) { - const snapshotResources = new Map(); + const snapshotIds = new Map(); self.addEventListener('install', function(event: any) { }); @@ -71,10 +71,6 @@ export class SnapshotServer { event.waitUntil(self.clients.claim()); }); - function respond404(): Response { - return new Response(null, { status: 404 }); - } - function respondNotAvailable(): Response { return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } }); } @@ -100,34 +96,26 @@ export class SnapshotServer { if (request.mode === 'navigate') { const htmlResponse = await fetch(event.request); - const { html, resources }: RenderedFrameSnapshot = await htmlResponse.json(); + const { html, frameId, index }: RenderedFrameSnapshot = await htmlResponse.json(); if (!html) return respondNotAvailable(); - snapshotResources.set(snapshotUrl, resources); + snapshotIds.set(snapshotUrl, { frameId, index }); const response = new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }); return response; } - const resources = snapshotResources.get(snapshotUrl)!; - const urlWithoutHash = removeHash(request.url); - const resource = resources[urlWithoutHash]; - if (!resource) - return respond404(); - - const fetchUrl = resource.sha1 ? - `/resources/${resource.resourceId}/override/${resource.sha1}` : - `/resources/${resource.resourceId}`; + const { frameId, index } = snapshotIds.get(snapshotUrl)!; + const url = removeHash(request.url); + const complexUrl = btoa(JSON.stringify({ frameId, index, url })); + const fetchUrl = `/resources/${complexUrl}`; const fetchedResponse = await fetch(fetchUrl); - const headers = new Headers(fetchedResponse.headers); // We make a copy of the response, instead of just forwarding, // so that response url is not inherited as "/resources/...", but instead // as the original request url. + // Response url turns into resource base uri that is used to resolve // relative links, e.g. url(/foo/bar) in style sheets. - if (resource.sha1) { - // No cache, so that we refetch overridden resources. - headers.set('Cache-Control', 'no-cache'); - } + const headers = new Headers(fetchedResponse.headers); const response = new Response(fetchedResponse.body, { status: fetchedResponse.status, statusText: fetchedResponse.statusText, @@ -178,32 +166,13 @@ export class SnapshotServer { } private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean { - // - /resources/ - // - /resources//override/ - const parts = request.url!.split('/'); - if (!parts[0]) - parts.shift(); - if (!parts[parts.length - 1]) - parts.pop(); - if (parts[0] !== 'resources') - return false; - - let resourceId; - let overrideSha1; - if (parts.length === 2) { - resourceId = parts[1]; - } else if (parts.length === 4 && parts[2] === 'override') { - resourceId = parts[1]; - overrideSha1 = parts[3]; - } else { - return false; - } - - const resource = this._snapshotStorage.resourceById(resourceId); + const { frameId, index, url } = JSON.parse(Buffer.from(request.url!.substring('/resources/'.length), 'base64').toString()); + const snapshot = this._snapshotStorage.snapshotByIndex(frameId, index); + const resource = snapshot?.resourceByUrl(url); if (!resource) return false; - const sha1 = overrideSha1 || resource.responseSha1; + const sha1 = resource.responseSha1; try { const content = this._snapshotStorage.resourceContent(sha1); if (!content) diff --git a/src/server/snapshot/snapshotStorage.ts b/src/server/snapshot/snapshotStorage.ts index dc1f5b7777..6234ab76d6 100644 --- a/src/server/snapshot/snapshotStorage.ts +++ b/src/server/snapshot/snapshotStorage.ts @@ -21,13 +21,12 @@ import { SnapshotRenderer } from './snapshotRenderer'; export interface SnapshotStorage { resources(): ResourceSnapshot[]; resourceContent(sha1: string): Buffer | undefined; - resourceById(resourceId: string): ResourceSnapshot | undefined; snapshotByName(pageOrFrameId: string, snapshotName: string): SnapshotRenderer | undefined; + snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined; } export abstract class BaseSnapshotStorage extends EventEmitter implements SnapshotStorage { protected _resources: ResourceSnapshot[] = []; - protected _resourceMap = new Map(); protected _frameSnapshots = new Map r.snapshotName === snapshotName); } + + snapshotByIndex(frameId: string, index: number): SnapshotRenderer | undefined { + const snapshot = this._frameSnapshots.get(frameId); + return snapshot?.renderer[index]; + } + } diff --git a/src/server/snapshot/snapshotTypes.ts b/src/server/snapshot/snapshotTypes.ts index c41c4a7264..cf5368fd08 100644 --- a/src/server/snapshot/snapshotTypes.ts +++ b/src/server/snapshot/snapshotTypes.ts @@ -15,7 +15,6 @@ */ export type ResourceSnapshot = { - resourceId: string, pageId: string, frameId: string, url: string, @@ -65,5 +64,7 @@ export type FrameSnapshot = { export type RenderedFrameSnapshot = { html: string; - resources: { [key: string]: { resourceId: string, sha1?: string } }; + pageId: string; + frameId: string; + index: number; }; diff --git a/src/server/snapshot/snapshotter.ts b/src/server/snapshot/snapshotter.ts index 85ea367593..49985acb9e 100644 --- a/src/server/snapshot/snapshotter.ts +++ b/src/server/snapshot/snapshotter.ts @@ -207,7 +207,6 @@ export class Snapshotter { const resource: ResourceSnapshot = { pageId: response.frame()._page.guid, frameId: response.frame().guid, - resourceId: response.guid, url, type: response.request().resourceType(), contentType, diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index 0275f58d2d..8098eb0bf6 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -49,11 +49,11 @@ export class TraceViewer { // - "/sha1/" - trace resource bodies, used by network previews. // // Served by SnapshotServer - // - "/resources/" - network resources from the trace. + // - "/resources/" - network resources from the trace. // - "/snapshot/" - root for snapshot frame. // - "/snapshot/pageId/..." - actual snapshot html. // - "/snapshot/service-worker.js" - service worker that intercepts snapshot resources - // and translates them into "/resources/". + // and translates them into network requests. const actionTraces = fs.readdirSync(tracesDir).filter(name => name.endsWith('.trace')); const debugNames = actionTraces.map(name => { const tracePrefix = path.join(tracesDir, name.substring(0, name.indexOf('.trace'))); diff --git a/tests/snapshotter.spec.ts b/tests/snapshotter.spec.ts index 738c5220aa..0c897afc32 100644 --- a/tests/snapshotter.spec.ts +++ b/tests/snapshotter.spec.ts @@ -62,9 +62,8 @@ it.describe('snapshots', () => { }); await page.setContent(''); const snapshot = await snapshotter.captureSnapshot(toImpl(page), 'snapshot'); - const { resources } = snapshot.render(); - const cssHref = `http://localhost:${server.PORT}/style.css`; - expect(resources[cssHref]).toBeTruthy(); + const resource = snapshot.resourceByUrl(`http://localhost:${server.PORT}/style.css`); + expect(resource).toBeTruthy(); }); it('should collect multiple', async ({ page, toImpl, snapshotter }) => { @@ -126,10 +125,8 @@ it.describe('snapshots', () => { await page.evaluate(() => { (document.styleSheets[0].cssRules[0] as any).style.color = 'blue'; }); const snapshot2 = await snapshotter.captureSnapshot(toImpl(page), 'snapshot1'); - const { resources } = snapshot2.render(); - const cssHref = `http://localhost:${server.PORT}/style.css`; - const { sha1 } = resources[cssHref]; - expect(snapshotter.resourceContent(sha1).toString()).toBe('button { color: blue; }'); + const resource = snapshot2.resourceByUrl(`http://localhost:${server.PORT}/style.css`); + expect(snapshotter.resourceContent(resource.responseSha1).toString()).toBe('button { color: blue; }'); }); it('should capture iframe', async ({ page, contextFactory, server, toImpl, browserName, snapshotter, snapshotPort }) => {