From 9dfc0a3394ddd0c755920573a71a8d33ff145941 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 11 Oct 2021 19:52:28 -0800 Subject: [PATCH] chore: make sw global in trace viewer (#9431) --- .../src/server/snapshot/snapshotServer.ts | 110 +++--------------- .../src/server/trace/viewer/traceViewer.ts | 19 ++- .../src/web/traceViewer/index.tsx | 1 + .../playwright-core/src/web/traceViewer/sw.ts | 88 ++++++++++++++ .../src/web/traceViewer/webpack-sw.config.js | 33 ++++++ tests/snapshotter.spec.ts | 6 +- utils/build/build.js | 3 +- utils/check_deps.js | 1 + 8 files changed, 157 insertions(+), 104 deletions(-) create mode 100644 packages/playwright-core/src/web/traceViewer/sw.ts create mode 100644 packages/playwright-core/src/web/traceViewer/webpack-sw.config.js diff --git a/packages/playwright-core/src/server/snapshot/snapshotServer.ts b/packages/playwright-core/src/server/snapshot/snapshotServer.ts index d2ca922dc9..7587276ffb 100644 --- a/packages/playwright-core/src/server/snapshot/snapshotServer.ts +++ b/packages/playwright-core/src/server/snapshot/snapshotServer.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import * as http from 'http'; -import querystring from 'querystring'; +import http from 'http'; +import path from 'path'; import { HttpServer } from '../../utils/httpServer'; -import type { RenderedFrameSnapshot, ResourceSnapshot } from './snapshotTypes'; +import type { ResourceSnapshot } from './snapshotTypes'; import { SnapshotStorage } from './snapshotStorage'; import type { Point } from '../../common/types'; +import { URLSearchParams } from 'url'; export class SnapshotServer { private _snapshotStorage: SnapshotStorage; @@ -27,6 +28,10 @@ export class SnapshotServer { constructor(server: HttpServer, snapshotStorage: SnapshotStorage) { this._snapshotStorage = snapshotStorage; + server.routePrefix('/snapshot/sw.bundle.js', (request, response) => { + server.serveFile(response, path.join(__dirname, '..', '..', 'web', 'traceViewer', 'sw.bundle.js')); + return true; + }); server.routePrefix('/snapshot/', this._serveSnapshot.bind(this)); server.routePrefix('/snapshotSize/', this._serveSnapshotSize.bind(this)); server.routePrefix('/resources/', this._serveResource.bind(this)); @@ -60,103 +65,25 @@ export class SnapshotServer { return true; } - private _serveServiceWorker(request: http.IncomingMessage, response: http.ServerResponse): boolean { - function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) { - const kBlobUrlPrefix = 'http://playwright.bloburl/#'; - const snapshotIds = new Map(); - - self.addEventListener('install', function(event: any) { - }); - - self.addEventListener('activate', function(event: any) { - event.waitUntil(self.clients.claim()); - }); - - function respondNotAvailable(): Response { - return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } }); - } - - function removeHash(url: string) { - try { - const u = new URL(url); - u.hash = ''; - return u.toString(); - } catch (e) { - return url; - } - } - - async function doFetch(event: any /* FetchEvent */): Promise { - const request = event.request; - const pathname = new URL(request.url).pathname; - if (pathname === '/snapshot/service-worker.js' || pathname === '/snapshot/') - return fetch(event.request); - - const snapshotUrl = request.mode === 'navigate' ? - request.url : (await self.clients.get(event.clientId))!.url; - - if (request.mode === 'navigate') { - const htmlResponse = await fetch(event.request); - const { html, frameId, index }: RenderedFrameSnapshot = await htmlResponse.json(); - if (!html) - return respondNotAvailable(); - snapshotIds.set(snapshotUrl, { frameId, index }); - const response = new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }); - return response; - } - - const { frameId, index } = snapshotIds.get(snapshotUrl)!; - const url = request.url.startsWith(kBlobUrlPrefix) ? request.url.substring(kBlobUrlPrefix.length) : removeHash(request.url); - const complexUrl = btoa(JSON.stringify({ frameId, index, url })); - const fetchUrl = `/resources/${complexUrl}`; - const fetchedResponse = await fetch(fetchUrl); - // 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. - const headers = new Headers(fetchedResponse.headers); - const response = new Response(fetchedResponse.body, { - status: fetchedResponse.status, - statusText: fetchedResponse.statusText, - headers, - }); - return response; - } - - self.addEventListener('fetch', function(event: any) { - event.respondWith(doFetch(event)); - }); - } - - response.statusCode = 200; - response.setHeader('Cache-Control', 'public, max-age=31536000'); - response.setHeader('Content-Type', 'application/javascript'); - response.end(`(${serviceWorkerMain.toString()})(self)`); - return true; - } - private _serveSnapshot(request: http.IncomingMessage, response: http.ServerResponse): boolean { - if (request.url!.endsWith('/snapshot/')) + const { pathname, searchParams } = new URL('http://localhost' + request.url); + if (pathname.endsWith('/snapshot/')) return this._serveSnapshotRoot(request, response); - if (request.url!.endsWith('/snapshot/service-worker.js')) - return this._serveServiceWorker(request, response); - const snapshot = this._snapshot(request.url!.substring('/snapshot/'.length)); + const snapshot = this._snapshot(pathname.substring('/snapshot'.length), searchParams); this._respondWithJson(response, snapshot ? snapshot.render() : { html: '' }); return true; } private _serveSnapshotSize(request: http.IncomingMessage, response: http.ServerResponse): boolean { - const snapshot = this._snapshot(request.url!.substring('/snapshotSize/'.length)); + const { pathname, searchParams } = new URL('http://localhost' + request.url); + const snapshot = this._snapshot(pathname.substring('/snapshotSize'.length), searchParams); this._respondWithJson(response, snapshot ? snapshot.viewport() : {}); return true; } - private _snapshot(uri: string) { - const [ pageOrFrameId, query ] = uri.split('?'); - const parsed: any = querystring.parse(query); - return this._snapshotStorage.snapshotByName(pageOrFrameId, parsed.name); + private _snapshot(pathname: string, params: URLSearchParams) { + const name = params.get('name')!; + return this._snapshotStorage.snapshotByName(pathname.slice(1), name); } private _respondWithJson(response: http.ServerResponse, object: any) { @@ -220,9 +147,8 @@ declare global { } } function rootScript() { - if (!navigator.serviceWorker) - return; - navigator.serviceWorker.register('./service-worker.js'); + if (window.location.href.endsWith('serviceWorkerForTest')) + navigator.serviceWorker.register('sw.bundle.js'); let showPromise = Promise.resolve(); if (!navigator.serviceWorker.controller) { showPromise = new Promise(resolve => { diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 230cd58d9b..54813458e2 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -50,7 +50,7 @@ export class TraceViewer { // - "/tracemodel" - json with trace model. // // Served by TraceViewer - // - "/traceviewer/..." - our frontend. + // - "/" - our frontend. // - "/file?filePath" - local files, used by sources tab. // - "/sha1/" - trace resource bodies, used by network previews. // @@ -58,7 +58,6 @@ export class TraceViewer { // - "/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 network requests. const entries = await this._vfs.entries(); const debugNames = entries.filter(name => name.endsWith('.trace')).map(name => { @@ -95,13 +94,6 @@ export class TraceViewer { }; this._server.routePrefix('/context/', traceModelHandler); - const traceViewerHandler: ServerRouteHandler = (request, response) => { - const relativePath = request.url!.substring('/traceviewer/'.length); - const absolutePath = path.join(__dirname, '..', '..', '..', 'web', ...relativePath.split('/')); - return this._server.serveFile(response, absolutePath); - }; - this._server.routePrefix('/traceviewer/', traceViewerHandler); - const fileHandler: ServerRouteHandler = (request, response) => { try { const url = new URL('http://localhost' + request.url!); @@ -123,6 +115,13 @@ export class TraceViewer { return true; }; this._server.routePrefix('/sha1/', sha1Handler); + + const traceViewerHandler: ServerRouteHandler = (request, response) => { + const relativePath = request.url!; + const absolutePath = path.join(__dirname, '..', '..', '..', 'web', 'traceViewer', ...relativePath.split('/')); + return this._server.serveFile(response, absolutePath); + }; + this._server.routePrefix('/', traceViewerHandler); } async show(headless: boolean): Promise { @@ -161,7 +160,7 @@ export class TraceViewer { else page.on('close', () => process.exit()); - await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/traceviewer/traceViewer/index.html'); + await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/index.html'); return context; } } diff --git a/packages/playwright-core/src/web/traceViewer/index.tsx b/packages/playwright-core/src/web/traceViewer/index.tsx index 8921e30a3e..5ee2fcdb29 100644 --- a/packages/playwright-core/src/web/traceViewer/index.tsx +++ b/packages/playwright-core/src/web/traceViewer/index.tsx @@ -23,6 +23,7 @@ import '../common.css'; (async () => { applyTheme(); + navigator.serviceWorker.register('sw.bundle.js'); const debugNames = await fetch('/contexts').then(response => response.json()); ReactDOM.render(, document.querySelector('#root')); })(); diff --git a/packages/playwright-core/src/web/traceViewer/sw.ts b/packages/playwright-core/src/web/traceViewer/sw.ts new file mode 100644 index 0000000000..d0aa2ab75e --- /dev/null +++ b/packages/playwright-core/src/web/traceViewer/sw.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { RenderedFrameSnapshot } from '../../server/snapshot/snapshotTypes'; + +// @ts-ignore +declare const self: ServiceWorkerGlobalScope; + +const kBlobUrlPrefix = 'http://playwright.bloburl/#'; +const snapshotIds = new Map(); + +self.addEventListener('install', function(event: any) { +}); + +self.addEventListener('activate', function(event: any) { + event.waitUntil(self.clients.claim()); +}); + +function respondNotAvailable(): Response { + return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } }); +} + +function removeHash(url: string) { + try { + const u = new URL(url); + u.hash = ''; + return u.toString(); + } catch (e) { + return url; + } +} + +async function doFetch(event: any /* FetchEvent */): Promise { + const request = event.request; + const pathname = new URL(request.url).pathname; + const isSnapshotUrl = pathname !== '/snapshot/' && pathname.startsWith('/snapshot/'); + if (request.url.startsWith(self.location.origin) && !isSnapshotUrl) + return fetch(event.request); + + const snapshotUrl = request.mode === 'navigate' ? + request.url : (await self.clients.get(event.clientId))!.url; + + if (request.mode === 'navigate') { + const htmlResponse = await fetch(request); + const { html, frameId, index }: RenderedFrameSnapshot = await htmlResponse.json(); + if (!html) + return respondNotAvailable(); + snapshotIds.set(snapshotUrl, { frameId, index }); + const response = new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }); + return response; + } + + const { frameId, index } = snapshotIds.get(snapshotUrl)!; + const url = request.url.startsWith(kBlobUrlPrefix) ? request.url.substring(kBlobUrlPrefix.length) : removeHash(request.url); + const complexUrl = btoa(JSON.stringify({ frameId, index, url })); + const fetchUrl = `/resources/${complexUrl}`; + const fetchedResponse = await fetch(fetchUrl); + // 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. + const headers = new Headers(fetchedResponse.headers); + const response = new Response(fetchedResponse.body, { + status: fetchedResponse.status, + statusText: fetchedResponse.statusText, + headers, + }); + return response; +} + +self.addEventListener('fetch', function(event: any) { + event.respondWith(doFetch(event)); +}); diff --git a/packages/playwright-core/src/web/traceViewer/webpack-sw.config.js b/packages/playwright-core/src/web/traceViewer/webpack-sw.config.js new file mode 100644 index 0000000000..a40edf489f --- /dev/null +++ b/packages/playwright-core/src/web/traceViewer/webpack-sw.config.js @@ -0,0 +1,33 @@ +const path = require('path'); + +const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'; +module.exports = { + mode, + entry: { + sw: path.join(__dirname, 'sw.ts'), + }, + resolve: { + extensions: ['.ts', '.js'] + }, + devtool: mode === 'production' ? false : 'source-map', + output: { + globalObject: 'self', + filename: '[name].bundle.js', + path: path.resolve(__dirname, '../../../lib/web/traceViewer') + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + loader: 'babel-loader', + options: { + presets: [ + "@babel/preset-typescript", + "@babel/preset-react" + ] + }, + exclude: /node_modules/ + }, + ] + }, +}; diff --git a/tests/snapshotter.spec.ts b/tests/snapshotter.spec.ts index 42b393dfd5..5c5a787c2c 100644 --- a/tests/snapshotter.spec.ts +++ b/tests/snapshotter.spec.ts @@ -19,6 +19,7 @@ import { InMemorySnapshotter } from 'playwright-core/lib/server/snapshot/inMemor import { HttpServer } from 'playwright-core/lib/utils/httpServer'; import { SnapshotServer } from 'playwright-core/lib/server/snapshot/snapshotServer'; import type { Frame } from 'playwright-core'; +import path from 'path'; const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnapshotter, showSnapshot: (snapshot: any) => Promise }>({ snapshotPort: async ({}, run, testInfo) => { @@ -30,6 +31,9 @@ const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnaps const snapshotter = new InMemorySnapshotter(toImpl(context)); await snapshotter.initialize(); const httpServer = new HttpServer(); + httpServer.routePath('/snapshot/sw.js', (request, response) => { + return httpServer.serveFile(response, path.join(__dirname, 'playwright-core/lib/web/traceViewer/sw.js')); + }); new SnapshotServer(httpServer, snapshotter); await httpServer.start(snapshotPort); await run(snapshotter); @@ -42,7 +46,7 @@ const it = contextTest.extend<{ snapshotPort: number, snapshotter: InMemorySnaps const previewContext = await contextFactory(); const previewPage = await previewContext.newPage(); previewPage.on('console', console.log); - await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/`); + await previewPage.goto(`http://localhost:${snapshotPort}/snapshot/?serviceWorkerForTest`); const frameSnapshot = snapshot.snapshot(); await previewPage.evaluate(snapshotId => { (window as any).showSnapshot(snapshotId); diff --git a/utils/build/build.js b/utils/build/build.js index 3f5710a9a6..a6f3f7d4a5 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -118,6 +118,7 @@ function copyFile(file, from, to) { const webPackFiles = [ 'packages/playwright-core/src/server/injected/webpack.config.js', 'packages/playwright-core/src/web/traceViewer/webpack.config.js', + 'packages/playwright-core/src/web/traceViewer/webpack-sw.config.js', 'packages/playwright-core/src/web/recorder/webpack.config.js', 'packages/playwright-core/src/web/htmlReport/webpack.config.js', ]; @@ -182,7 +183,7 @@ copyFiles.push({ files: 'packages/playwright-core/src/**/*.js', from: 'packages/playwright-core/src', to: 'packages/playwright-core/lib', - ignored: ['**/.eslintrc.js', '**/*webpack.config.js', '**/injected/**/*'] + ignored: ['**/.eslintrc.js', '**/webpack*.config.js', '**/injected/**/*'] }); // Sometimes we require JSON files that babel ignores. diff --git a/utils/check_deps.js b/utils/check_deps.js index ed819f2bb3..2e7d34fa4a 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -176,6 +176,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inProcessFactory.ts'] = DEPS['src/browserS // Tracing is a client/server plugin, nothing should depend on it. DEPS['src/web/recorder/'] = ['src/common/', 'src/web/', 'src/web/components/', 'src/server/supplements/recorder/recorderTypes.ts']; DEPS['src/web/traceViewer/'] = ['src/common/', 'src/web/']; +DEPS['src/web/traceViewer/sw.ts'] = ['src/server/snapshot/snapshotTypes.ts']; DEPS['src/web/traceViewer/ui/'] = ['src/common/', 'src/protocol/', 'src/web/traceViewer/', 'src/web/', 'src/server/trace/viewer/', 'src/server/trace/', 'src/server/trace/common/', 'src/server/snapshot/snapshotTypes.ts', 'src/protocol/channels.ts']; // The service is a cross-cutting feature, and so it depends on a bunch of things. DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/server/trace/', 'src/utils/**'];