diff --git a/src/cli/traceViewer/screenshotGenerator.ts b/src/cli/traceViewer/screenshotGenerator.ts index 2ed6329e29..ba4b8dc805 100644 --- a/src/cli/traceViewer/screenshotGenerator.ts +++ b/src/cli/traceViewer/screenshotGenerator.ts @@ -18,31 +18,31 @@ import * as fs from 'fs'; import * as path from 'path'; import * as playwright from '../../..'; import * as util from 'util'; -import { actionById, ActionEntry, ContextEntry, PageEntry, TraceModel } from './traceModel'; +import { actionById, ActionEntry, ContextEntry, TraceModel } from './traceModel'; import { SnapshotServer } from './snapshotServer'; const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); export class ScreenshotGenerator { - private _traceStorageDir: string; + private _resourcesDir: string; private _browserPromise: Promise; - private _serverPromise: Promise; + private _snapshotServer: SnapshotServer; private _traceModel: TraceModel; private _rendering = new Map>(); private _lock = new Lock(3); - constructor(resourcesDir: string, traceModel: TraceModel) { - this._traceStorageDir = resourcesDir; + constructor(snapshotServer: SnapshotServer, resourcesDir: string, traceModel: TraceModel) { + this._snapshotServer = snapshotServer; + this._resourcesDir = resourcesDir; this._traceModel = traceModel; this._browserPromise = playwright.chromium.launch(); - this._serverPromise = SnapshotServer.create(undefined, resourcesDir, traceModel, undefined); } generateScreenshot(actionId: string): Promise { - const { context, action, page } = actionById(this._traceModel, actionId); + const { context, action } = actionById(this._traceModel, actionId); if (!this._rendering.has(action)) { - this._rendering.set(action, this._render(context, page, action).then(body => { + this._rendering.set(action, this._render(context, action).then(body => { this._rendering.delete(action); return body; })); @@ -50,8 +50,8 @@ export class ScreenshotGenerator { return this._rendering.get(action)!; } - private async _render(contextEntry: ContextEntry, pageEntry: PageEntry, actionEntry: ActionEntry): Promise { - const imageFileName = path.join(this._traceStorageDir, actionEntry.action.timestamp + '-screenshot.png'); + private async _render(contextEntry: ContextEntry, actionEntry: ActionEntry): Promise { + const imageFileName = path.join(this._resourcesDir, actionEntry.action.timestamp + '-screenshot.png'); try { return await fsReadFileAsync(imageFileName); } catch (e) { @@ -60,7 +60,6 @@ export class ScreenshotGenerator { const { action } = actionEntry; const browser = await this._browserPromise; - const server = await this._serverPromise; await this._lock.obtain(); @@ -70,15 +69,11 @@ export class ScreenshotGenerator { }); try { - await page.goto(server.snapshotRootUrl()); - await page.evaluate(async () => { - navigator.serviceWorker.register('/service-worker.js'); - await new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve); - }); + await page.goto(this._snapshotServer.snapshotRootUrl()); const snapshots = action.snapshots || []; const snapshotId = snapshots.length ? snapshots[0].snapshotId : undefined; - const snapshotUrl = server.snapshotUrl(action.pageId!, snapshotId, action.endTime); + const snapshotUrl = this._snapshotServer.snapshotUrl(action.pageId!, snapshotId, action.endTime); console.log('Generating screenshot for ' + action.action); // eslint-disable-line no-console await page.evaluate(snapshotUrl => (window as any).showSnapshot(snapshotUrl), snapshotUrl); diff --git a/src/cli/traceViewer/snapshotServer.ts b/src/cli/traceViewer/snapshotServer.ts index 8b1e94cd17..4d93a8fda9 100644 --- a/src/cli/traceViewer/snapshotServer.ts +++ b/src/cli/traceViewer/snapshotServer.ts @@ -18,29 +18,16 @@ import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; import type { TraceModel, trace } from './traceModel'; -import type { ScreenshotGenerator } from './screenshotGenerator'; +import { TraceServer } from './traceServer'; export class SnapshotServer { - static async create(traceViewerDir: string | undefined, resourcesDir: string | undefined, traceModel: TraceModel, screenshotGenerator: ScreenshotGenerator | undefined): Promise { - const server = new SnapshotServer(traceViewerDir, resourcesDir, traceModel, screenshotGenerator); - await new Promise(cb => server._server.once('listening', cb)); - return server; - } - - private _traceViewerDir: string | undefined; private _resourcesDir: string | undefined; - private _traceModel: TraceModel; - private _server: http.Server; + private _server: TraceServer; private _resourceById: Map; - private _screenshotGenerator: ScreenshotGenerator | undefined; - constructor(traceViewerDir: string | undefined, resourcesDir: string | undefined, traceModel: TraceModel, screenshotGenerator: ScreenshotGenerator | undefined) { - this._traceViewerDir = traceViewerDir; + constructor(server: TraceServer, traceModel: TraceModel, resourcesDir: string | undefined) { this._resourcesDir = resourcesDir; - this._traceModel = traceModel; - this._screenshotGenerator = screenshotGenerator; - this._server = http.createServer(this._onRequest.bind(this)); - this._server.listen(); + this._server = server; this._resourceById = new Map(); for (const contextEntry of traceModel.contexts) { @@ -50,74 +37,25 @@ export class SnapshotServer { pageEntry.resources.forEach(r => this._resourceById.set(r.resourceId, r)); } } - } - private _urlPrefix() { - const address = this._server.address(); - return typeof address === 'string' ? address : `http://127.0.0.1:${address.port}`; - } - - traceViewerUrl(relative: string) { - return this._urlPrefix() + '/traceviewer/' + relative; + server.routePath('/snapshot/', this._serveSnapshotRoot.bind(this), true); + server.routePath('/snapshot/service-worker.js', this._serveServiceWorker.bind(this)); + server.routePrefix('/resources/', this._serveResource.bind(this)); } snapshotRootUrl() { - return this._urlPrefix() + '/snapshot/'; + return this._server.urlPrefix() + '/snapshot/'; } snapshotUrl(pageId: string, snapshotId?: string, timestamp?: number) { + // Prefer snapshotId over timestamp. if (snapshotId) - return this._urlPrefix() + `/snapshot/pageId/${pageId}/snapshotId/${snapshotId}/main`; + return this._server.urlPrefix() + `/snapshot/pageId/${pageId}/snapshotId/${snapshotId}/main`; if (timestamp) - return this._urlPrefix() + `/snapshot/pageId/${pageId}/timestamp/${timestamp}/main`; + return this._server.urlPrefix() + `/snapshot/pageId/${pageId}/timestamp/${timestamp}/main`; return 'data:text/html,Snapshot is not available'; } - private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) { - // This server serves: - // - "/traceviewer/..." - our frontend; - // - "/sha1/" - trace resources; - // - "/tracemodel" - json with trace model; - // - "/resources/" - network resources from the trace; - // - "/file?filePath" - local files for sources tab; - // - "/action-preview/..." - lazily generated action previews; - // - "/snapshot/" - root for snapshot frame; - // - "/snapshot/pageId/..." - actual snapshot html; - // - "/service-worker.js" - service worker that intercepts snapshot resources - // and translates them into "/resources/". - - request.on('error', () => response.end()); - if (!request.url) - return response.end(); - - const url = new URL('http://localhost' + request.url); - // These two entry points do not require referrer check. - if (url.pathname.startsWith('/traceviewer/') && this._serveTraceViewer(request, response, url.pathname)) - return; - if (url.pathname === '/snapshot/' && this._serveSnapshotRoot(request, response)) - return; - - // Only serve the rest when referrer is present to avoid exposure. - const hasReferrer = request.headers['referer'] && request.headers['referer'].startsWith(this._urlPrefix()); - if (!hasReferrer) - return response.end(); - if (url.pathname.startsWith('/resources/') && this._serveResource(request, response, url.pathname)) - return; - if (url.pathname.startsWith('/sha1/') && this._serveSha1(request, response, url.pathname)) - return; - if (url.pathname.startsWith('/action-preview/') && this._serveActionPreview(request, response, url.pathname)) - return; - if (url.pathname === '/file' && this._serveFile(request, response, url.search)) - return; - if (url.pathname === '/service-worker.js' && this._serveServiceWorker(request, response)) - return; - if (url.pathname === '/tracemodel' && this._serveTraceModel(request, response)) - return; - - response.statusCode = 404; - response.end(); - } - private _serveSnapshotRoot(request: http.IncomingMessage, response: http.ServerResponse): boolean { response.statusCode = 200; response.setHeader('Cache-Control', 'public, max-age=31536000'); @@ -139,13 +77,18 @@ export class SnapshotServer {