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) {