diff --git a/src/server/trace/common/traceEvents.ts b/src/server/trace/common/traceEvents.ts index 9298308e90..1279342e23 100644 --- a/src/server/trace/common/traceEvents.ts +++ b/src/server/trace/common/traceEvents.ts @@ -37,7 +37,6 @@ export type ContextCreatedTraceEvent = { isMobile: boolean, viewportSize?: { width: number, height: number }, debugName?: string, - snapshotScript: string, }; export type ContextDestroyedTraceEvent = { diff --git a/src/server/trace/recorder/snapshotterInjected.ts b/src/server/trace/recorder/snapshotterInjected.ts index 8e90611a75..8ce1555f07 100644 --- a/src/server/trace/recorder/snapshotterInjected.ts +++ b/src/server/trace/recorder/snapshotterInjected.ts @@ -427,59 +427,3 @@ export function frameSnapshotStreamer() { (window as any)[kSnapshotStreamer] = new Streamer(); } - -export function snapshotScript() { - function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string) { - const scrollTops: Element[] = []; - const scrollLefts: Element[] = []; - - const visit = (root: Document | ShadowRoot) => { - // Collect all scrolled elements for later use. - for (const e of root.querySelectorAll(`[${scrollTopAttribute}]`)) - scrollTops.push(e); - for (const e of root.querySelectorAll(`[${scrollLeftAttribute}]`)) - scrollLefts.push(e); - - for (const iframe of root.querySelectorAll('iframe')) { - const src = iframe.getAttribute('src') || ''; - if (src.startsWith('data:text/html')) - continue; - // Rewrite iframes to use snapshot url (relative to window.location) - // instead of begin relative to the tag. - const index = location.pathname.lastIndexOf('/'); - if (index === -1) - continue; - const pathname = location.pathname.substring(0, index + 1) + src; - const href = location.href.substring(0, location.href.indexOf(location.pathname)) + pathname; - iframe.setAttribute('src', href); - } - - for (const element of root.querySelectorAll(`template[${shadowAttribute}]`)) { - const template = element as HTMLTemplateElement; - const shadowRoot = template.parentElement!.attachShadow({ mode: 'open' }); - shadowRoot.appendChild(template.content); - template.remove(); - visit(shadowRoot); - } - }; - visit(document); - - const onLoad = () => { - window.removeEventListener('load', onLoad); - for (const element of scrollTops) { - element.scrollTop = +element.getAttribute(scrollTopAttribute)!; - element.removeAttribute(scrollTopAttribute); - } - for (const element of scrollLefts) { - element.scrollLeft = +element.getAttribute(scrollLeftAttribute)!; - element.removeAttribute(scrollLeftAttribute); - } - }; - window.addEventListener('load', onLoad); - } - - const kShadowAttribute = '__playwright_shadow_root_'; - const kScrollTopAttribute = '__playwright_scroll_top_'; - const kScrollLeftAttribute = '__playwright_scroll_left_'; - return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}')`; -} diff --git a/src/server/trace/recorder/tracer.ts b/src/server/trace/recorder/tracer.ts index 5e97c54469..651031e417 100644 --- a/src/server/trace/recorder/tracer.ts +++ b/src/server/trace/recorder/tracer.ts @@ -26,7 +26,6 @@ import { Snapshotter } from './snapshotter'; import { helper, RegisteredListener } from '../../helper'; import { Dialog } from '../../dialog'; import { Frame, NavigationEvent } from '../../frames'; -import { snapshotScript } from './snapshotterInjected'; import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); @@ -102,7 +101,6 @@ class ContextTracer implements SnapshotterDelegate { deviceScaleFactor: context._options.deviceScaleFactor || 1, viewportSize: context._options.viewport || undefined, debugName: context._options._debugName, - snapshotScript: snapshotScript(), }; this._appendTraceEvent(event); this._snapshotter = new Snapshotter(context, this); diff --git a/src/server/trace/viewer/frameSnapshot.ts b/src/server/trace/viewer/frameSnapshot.ts index 7885ab26da..97e9b719f1 100644 --- a/src/server/trace/viewer/frameSnapshot.ts +++ b/src/server/trace/viewer/frameSnapshot.ts @@ -15,7 +15,7 @@ */ import * as trace from '../common/traceEvents'; -import { ContextEntry, ContextResources } from './traceModel'; +import { ContextResources } from './traceModel'; export * as trace from '../common/traceEvents'; export type SerializedFrameSnapshot = { @@ -26,13 +26,11 @@ export type SerializedFrameSnapshot = { export class FrameSnapshot { private _snapshots: trace.FrameSnapshotTraceEvent[]; private _index: number; - private _contextEntry: ContextEntry; private _contextResources: ContextResources; private _frameId: string; - constructor(frameId: string, contextEntry: ContextEntry, contextResources: ContextResources, events: trace.FrameSnapshotTraceEvent[], index: number) { + constructor(frameId: string, contextResources: ContextResources, events: trace.FrameSnapshotTraceEvent[], index: number) { this._frameId = frameId; - this._contextEntry = contextEntry; this._contextResources = contextResources; this._snapshots = events; this._index = index; @@ -82,7 +80,7 @@ export class FrameSnapshot { let html = visit(snapshot.html, this._index); if (snapshot.doctype) html = `` + html; - html += ``; + html += ``; const resources: { [key: string]: { resourceId: string, sha1?: string } } = {}; for (const [url, contextResources] of this._contextResources) { @@ -125,3 +123,59 @@ function snapshotNodes(snapshot: trace.FrameSnapshot): trace.NodeSnapshot[] { } return (snapshot as any)._nodes; } + +export function snapshotScript() { + function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string) { + const scrollTops: Element[] = []; + const scrollLefts: Element[] = []; + + const visit = (root: Document | ShadowRoot) => { + // Collect all scrolled elements for later use. + for (const e of root.querySelectorAll(`[${scrollTopAttribute}]`)) + scrollTops.push(e); + for (const e of root.querySelectorAll(`[${scrollLeftAttribute}]`)) + scrollLefts.push(e); + + for (const iframe of root.querySelectorAll('iframe')) { + const src = iframe.getAttribute('src') || ''; + if (src.startsWith('data:text/html')) + continue; + // Rewrite iframes to use snapshot url (relative to window.location) + // instead of begin relative to the tag. + const index = location.pathname.lastIndexOf('/'); + if (index === -1) + continue; + const pathname = location.pathname.substring(0, index + 1) + src; + const href = location.href.substring(0, location.href.indexOf(location.pathname)) + pathname; + iframe.setAttribute('src', href); + } + + for (const element of root.querySelectorAll(`template[${shadowAttribute}]`)) { + const template = element as HTMLTemplateElement; + const shadowRoot = template.parentElement!.attachShadow({ mode: 'open' }); + shadowRoot.appendChild(template.content); + template.remove(); + visit(shadowRoot); + } + }; + visit(document); + + const onLoad = () => { + window.removeEventListener('load', onLoad); + for (const element of scrollTops) { + element.scrollTop = +element.getAttribute(scrollTopAttribute)!; + element.removeAttribute(scrollTopAttribute); + } + for (const element of scrollLefts) { + element.scrollLeft = +element.getAttribute(scrollLeftAttribute)!; + element.removeAttribute(scrollLeftAttribute); + } + }; + window.addEventListener('load', onLoad); + } + + const kShadowAttribute = '__playwright_shadow_root_'; + const kScrollTopAttribute = '__playwright_scroll_top_'; + const kScrollLeftAttribute = '__playwright_scroll_left_'; + return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}')`; +} diff --git a/src/server/trace/viewer/snapshotServer.ts b/src/server/trace/viewer/snapshotServer.ts index bf32c21537..91826f0974 100644 --- a/src/server/trace/viewer/snapshotServer.ts +++ b/src/server/trace/viewer/snapshotServer.ts @@ -15,25 +15,22 @@ */ import * as http from 'http'; -import fs from 'fs'; -import path from 'path'; import querystring from 'querystring'; -import { TraceServer } from './traceServer'; -import type { FrameSnapshot, SerializedFrameSnapshot } from './frameSnapshot'; import type { NetworkResourceTraceEvent } from '../common/traceEvents'; +import type { FrameSnapshot, SerializedFrameSnapshot } from './frameSnapshot'; +import { HttpServer } from '../../../utils/httpServer'; export interface SnapshotStorage { + resourceContent(sha1: string): Buffer; resourceById(resourceId: string): NetworkResourceTraceEvent; snapshotByName(snapshotName: string): FrameSnapshot | undefined; } export class SnapshotServer { - private _resourcesDir: string | undefined; private _urlPrefix: string; private _snapshotStorage: SnapshotStorage; - constructor(server: TraceServer, snapshotStorage: SnapshotStorage, resourcesDir: string | undefined) { - this._resourcesDir = resourcesDir; + constructor(server: HttpServer, snapshotStorage: SnapshotStorage) { this._urlPrefix = server.urlPrefix(); this._snapshotStorage = snapshotStorage; @@ -212,9 +209,6 @@ export class SnapshotServer { } private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean { - if (!this._resourcesDir) - return false; - // - /resources/ // - /resources//override/ const parts = request.url!.split('/'); @@ -239,7 +233,7 @@ export class SnapshotServer { const resource = this._snapshotStorage.resourceById(resourceId); const sha1 = overrideSha1 || resource.responseSha1; try { - const content = fs.readFileSync(path.join(this._resourcesDir, sha1)); + const content = this._snapshotStorage.resourceContent(sha1); response.statusCode = 200; let contentType = resource.contentType; const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType); diff --git a/src/server/trace/viewer/traceModel.ts b/src/server/trace/viewer/traceModel.ts index 8a65a29a50..c23440da69 100644 --- a/src/server/trace/viewer/traceModel.ts +++ b/src/server/trace/viewer/traceModel.ts @@ -157,7 +157,7 @@ export class TraceModel { const frameSnapshots = pageEntry.snapshotsByFrameId[frameId]; for (let index = 0; index < frameSnapshots.length; index++) { if (frameSnapshots[index].snapshotId === snapshotId) - return new FrameSnapshot(frameId, contextEntry, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, index); + return new FrameSnapshot(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, index); } } @@ -170,7 +170,7 @@ export class TraceModel { if (timestamp && snapshot.timestamp <= timestamp) snapshotIndex = index; } - return snapshotIndex >= 0 ? new FrameSnapshot(frameId, contextEntry, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, snapshotIndex) : undefined; + return snapshotIndex >= 0 ? new FrameSnapshot(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, snapshotIndex) : undefined; } } diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index 121814d6e8..1e26f2704a 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -22,7 +22,7 @@ import { ScreenshotGenerator } from './screenshotGenerator'; import { TraceModel } from './traceModel'; import { NetworkResourceTraceEvent, TraceEvent } from '../common/traceEvents'; import { SnapshotServer, SnapshotStorage } from './snapshotServer'; -import { ServerRouteHandler, TraceServer } from './traceServer'; +import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer'; import { FrameSnapshot } from './frameSnapshot'; const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); @@ -32,8 +32,6 @@ type TraceViewerDocument = { model: TraceModel; }; -const emptyModel: TraceModel = new TraceModel(); - class TraceViewer implements SnapshotStorage { private _document: TraceViewerDocument | undefined; @@ -74,8 +72,17 @@ class TraceViewer implements SnapshotStorage { // - "/snapshot/service-worker.js" - service worker that intercepts snapshot resources // and translates them into "/resources/". - const server = new TraceServer(this._document ? this._document.model : emptyModel); - const snapshotServer = new SnapshotServer(server, this, this._document ? this._document.resourcesDir : undefined); + const server = new HttpServer(); + + const traceModelHandler: ServerRouteHandler = (request, response) => { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify(Array.from(this._document!.model.contextEntries.values()))); + return true; + }; + server.routePath('/contexts', traceModelHandler); + + const snapshotServer = new SnapshotServer(server, this); const screenshotGenerator = this._document ? new ScreenshotGenerator(snapshotServer, this._document.resourcesDir, this._document.model) : undefined; const traceViewerHandler: ServerRouteHandler = (request, response) => { @@ -145,6 +152,10 @@ class TraceViewer implements SnapshotStorage { const snapshot = parsed.snapshotId ? traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) : traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!); return snapshot; } + + resourceContent(sha1: string): Buffer { + return fs.readFileSync(path.join(this._document!.resourcesDir, sha1)); + } } export async function showTraceViewer(traceDir: string) { diff --git a/src/server/trace/viewer/traceServer.ts b/src/utils/httpServer.ts similarity index 87% rename from src/server/trace/viewer/traceServer.ts rename to src/utils/httpServer.ts index b171ecfaeb..1a2d62a632 100644 --- a/src/server/trace/viewer/traceServer.ts +++ b/src/utils/httpServer.ts @@ -17,27 +17,16 @@ import * as http from 'http'; import fs from 'fs'; import path from 'path'; -import type { TraceModel } from './traceModel'; export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean; -export class TraceServer { - private _traceModel: TraceModel; +export class HttpServer { private _server: http.Server | undefined; private _urlPrefix: string; private _routes: { prefix?: string, exact?: string, needsReferrer: boolean, handler: ServerRouteHandler }[] = []; - constructor(traceModel: TraceModel) { - this._traceModel = traceModel; + constructor() { this._urlPrefix = ''; - - const traceModelHandler: ServerRouteHandler = (request, response) => { - response.statusCode = 200; - response.setHeader('Content-Type', 'application/json'); - response.end(JSON.stringify(Array.from(this._traceModel.contextEntries.values()))); - return true; - }; - this.routePath('/contexts', traceModelHandler); } routePrefix(prefix: string, handler: ServerRouteHandler, skipReferrerCheck?: boolean) {