chore: make trace server generic (#5616)
This commit is contained in:
parent
1cd398e700
commit
af89ab7a6f
|
|
@ -37,7 +37,6 @@ export type ContextCreatedTraceEvent = {
|
|||
isMobile: boolean,
|
||||
viewportSize?: { width: number, height: number },
|
||||
debugName?: string,
|
||||
snapshotScript: string,
|
||||
};
|
||||
|
||||
export type ContextDestroyedTraceEvent = {
|
||||
|
|
|
|||
|
|
@ -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 <base> 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}')`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = `<!DOCTYPE ${snapshot.doctype}>` + html;
|
||||
html += `<script>${this._contextEntry.created.snapshotScript}</script>`;
|
||||
html += `<script>${snapshotScript}</script>`;
|
||||
|
||||
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 <base> 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}')`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/<resourceId>
|
||||
// - /resources/<resourceId>/override/<overrideSha1>
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<resourceId>".
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
Loading…
Reference in a new issue