diff --git a/src/server/trace/viewer/frameSnapshot.ts b/src/server/trace/viewer/frameSnapshot.ts
new file mode 100644
index 0000000000..3d5c68d811
--- /dev/null
+++ b/src/server/trace/viewer/frameSnapshot.ts
@@ -0,0 +1,120 @@
+/**
+ * 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 * as trace from '../common/traceEvents';
+import { ContextEntry } from './traceModel';
+export * as trace from '../common/traceEvents';
+
+export type SerializedFrameSnapshot = {
+ html: string;
+ resourcesByUrl: { [key: string]: { resourceId: string, frameId: string }[] };
+ overridenUrls: { [key: string]: boolean };
+ resourceOverrides: { [key: string]: string };
+};
+
+export class FrameSnapshot {
+ private _snapshots: trace.FrameSnapshotTraceEvent[];
+ private _index: number;
+ contextEntry: ContextEntry;
+
+ constructor(contextEntry: ContextEntry, events: trace.FrameSnapshotTraceEvent[], index: number) {
+ this.contextEntry = contextEntry;
+ this._snapshots = events;
+ this._index = index;
+ }
+
+ traceEvent(): trace.FrameSnapshotTraceEvent {
+ return this._snapshots[this._index];
+ }
+
+ serialize(): SerializedFrameSnapshot {
+ const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
+ // Text node.
+ if (typeof n === 'string')
+ return escapeText(n);
+
+ if (!(n as any)._string) {
+ if (Array.isArray(n[0])) {
+ // Node reference.
+ const referenceIndex = snapshotIndex - n[0][0];
+ if (referenceIndex >= 0 && referenceIndex < snapshotIndex) {
+ const nodes = snapshotNodes(this._snapshots[referenceIndex].snapshot);
+ const nodeIndex = n[0][1];
+ if (nodeIndex >= 0 && nodeIndex < nodes.length)
+ (n as any)._string = visit(nodes[nodeIndex], referenceIndex);
+ }
+ } else if (typeof n[0] === 'string') {
+ // Element node.
+ const builder: string[] = [];
+ builder.push('<', n[0]);
+ for (const [attr, value] of Object.entries(n[1] || {}))
+ builder.push(' ', attr, '="', escapeAttribute(value as string), '"');
+ builder.push('>');
+ for (let i = 2; i < n.length; i++)
+ builder.push(visit(n[i], snapshotIndex));
+ if (!autoClosing.has(n[0]))
+ builder.push('', n[0], '>');
+ (n as any)._string = builder.join('');
+ } else {
+ // Why are we here? Let's not throw, just in case.
+ (n as any)._string = '';
+ }
+ }
+ return (n as any)._string;
+ };
+
+ const snapshot = this._snapshots[this._index].snapshot;
+ let html = visit(snapshot.html, this._index);
+ if (snapshot.doctype)
+ html = `` + html;
+ html += ``;
+
+ const resourcesByUrl = this.contextEntry.resourcesByUrl;
+ const overridenUrls = this.contextEntry.overridenUrls;
+ const resourceOverrides: any = {};
+ for (const o of this._snapshots[this._index].snapshot.resourceOverrides)
+ resourceOverrides[o.url] = o.sha1;
+ return { html, resourcesByUrl, overridenUrls, resourceOverrides };
+ }
+}
+
+const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
+const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
+
+function escapeAttribute(s: string): string {
+ return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
+}
+function escapeText(s: string): string {
+ return s.replace(/[&<]/ug, char => (escaped as any)[char]);
+}
+
+function snapshotNodes(snapshot: trace.FrameSnapshot): trace.NodeSnapshot[] {
+ if (!(snapshot as any)._nodes) {
+ const nodes: trace.NodeSnapshot[] = [];
+ const visit = (n: trace.NodeSnapshot) => {
+ if (typeof n === 'string') {
+ nodes.push(n);
+ } else if (typeof n[0] === 'string') {
+ for (let i = 2; i < n.length; i++)
+ visit(n[i]);
+ nodes.push(n);
+ }
+ };
+ visit(snapshot.html);
+ (snapshot as any)._nodes = nodes;
+ }
+ return (snapshot as any)._nodes;
+}
diff --git a/src/server/trace/viewer/screenshotGenerator.ts b/src/server/trace/viewer/screenshotGenerator.ts
index 03b4ed02ad..90f589d971 100644
--- a/src/server/trace/viewer/screenshotGenerator.ts
+++ b/src/server/trace/viewer/screenshotGenerator.ts
@@ -18,7 +18,7 @@ import fs from 'fs';
import path from 'path';
import * as playwright from '../../../..';
import * as util from 'util';
-import { actionById, ActionEntry, ContextEntry, TraceModel } from './traceModel';
+import { ActionEntry, ContextEntry, TraceModel } from './traceModel';
import { SnapshotServer } from './snapshotServer';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
@@ -40,7 +40,7 @@ export class ScreenshotGenerator {
}
generateScreenshot(actionId: string): Promise {
- const { context, action } = actionById(this._traceModel, actionId);
+ const { context, action } = this._traceModel.actionById(actionId);
if (!this._rendering.has(action)) {
this._rendering.set(action, this._render(context, action).then(body => {
this._rendering.delete(action);
diff --git a/src/server/trace/viewer/snapshotServer.ts b/src/server/trace/viewer/snapshotServer.ts
index 8ff97425d8..553a6b94a6 100644
--- a/src/server/trace/viewer/snapshotServer.ts
+++ b/src/server/trace/viewer/snapshotServer.ts
@@ -19,29 +19,18 @@ import fs from 'fs';
import path from 'path';
import querystring from 'querystring';
import type { TraceModel } from './traceModel';
-import * as trace from '../common/traceEvents';
import { TraceServer } from './traceServer';
export class SnapshotServer {
private _resourcesDir: string | undefined;
private _server: TraceServer;
- private _resourceById: Map;
private _traceModel: TraceModel;
constructor(server: TraceServer, traceModel: TraceModel, resourcesDir: string | undefined) {
this._resourcesDir = resourcesDir;
this._server = server;
- this._resourceById = new Map();
this._traceModel = traceModel;
- for (const contextEntry of traceModel.contexts) {
- for (const pageEntry of contextEntry.pages) {
- for (const action of pageEntry.actions)
- action.resources.forEach(r => this._resourceById.set(r.resourceId, r));
- pageEntry.resources.forEach(r => this._resourceById.set(r.resourceId, r));
- }
- }
-
server.routePath('/snapshot/', this._serveSnapshotRoot.bind(this), true);
server.routePath('/snapshot/service-worker.js', this._serveServiceWorker.bind(this));
server.routePath('/snapshot-data', this._serveSnapshot.bind(this));
@@ -113,86 +102,6 @@ export class SnapshotServer {
return true;
}
- private _frameSnapshotData(parsed: { pageId: string, frameId: string, snapshotId?: string, timestamp?: number }) {
- let contextEntry;
- let pageEntry;
- for (const c of this._traceModel.contexts) {
- for (const p of c.pages) {
- if (p.created.pageId === parsed.pageId) {
- contextEntry = c;
- pageEntry = p;
- }
- }
- }
- if (!contextEntry || !pageEntry)
- return { html: '' };
-
- const frameSnapshots = pageEntry.snapshotsByFrameId[parsed.frameId] || [];
- let snapshotIndex = -1;
- for (let index = 0; index < frameSnapshots.length; index++) {
- const current = snapshotIndex === -1 ? undefined : frameSnapshots[snapshotIndex];
- const snapshot = frameSnapshots[index];
- // Prefer snapshot with exact id.
- const exactMatch = parsed.snapshotId && snapshot.snapshotId === parsed.snapshotId;
- const currentExactMatch = current && parsed.snapshotId && current.snapshotId === parsed.snapshotId;
- // If not available, prefer the latest snapshot before the timestamp.
- const timestampMatch = parsed.timestamp && snapshot.timestamp <= parsed.timestamp;
- if (exactMatch || (timestampMatch && !currentExactMatch))
- snapshotIndex = index;
- }
- let html = this._serializeSnapshot(frameSnapshots, snapshotIndex);
- html += ``;
- const resourcesByUrl = contextEntry.resourcesByUrl;
- const overridenUrls = contextEntry.overridenUrls;
- const resourceOverrides: any = {};
- for (const o of frameSnapshots[snapshotIndex].snapshot.resourceOverrides)
- resourceOverrides[o.url] = o.sha1;
- return { html, resourcesByUrl, overridenUrls, resourceOverrides };
- }
-
- private _serializeSnapshot(snapshots: trace.FrameSnapshotTraceEvent[], initialSnapshotIndex: number): string {
- const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
- // Text node.
- if (typeof n === 'string')
- return escapeText(n);
-
- if (!(n as any)._string) {
- if (Array.isArray(n[0])) {
- // Node reference.
- const referenceIndex = snapshotIndex - n[0][0];
- if (referenceIndex >= 0 && referenceIndex < snapshotIndex) {
- const nodes = snapshotNodes(snapshots[referenceIndex].snapshot);
- const nodeIndex = n[0][1];
- if (nodeIndex >= 0 && nodeIndex < nodes.length)
- (n as any)._string = visit(nodes[nodeIndex], referenceIndex);
- }
- } else if (typeof n[0] === 'string') {
- // Element node.
- const builder: string[] = [];
- builder.push('<', n[0]);
- for (const [attr, value] of Object.entries(n[1] || {}))
- builder.push(' ', attr, '="', escapeAttribute(value as string), '"');
- builder.push('>');
- for (let i = 2; i < n.length; i++)
- builder.push(visit(n[i], snapshotIndex));
- if (!autoClosing.has(n[0]))
- builder.push('', n[0], '>');
- (n as any)._string = builder.join('');
- } else {
- // Why are we here? Let's not throw, just in case.
- (n as any)._string = '';
- }
- }
- return (n as any)._string;
- };
-
- const snapshot = snapshots[initialSnapshotIndex].snapshot;
- let html = visit(snapshot.html, initialSnapshotIndex);
- if (snapshot.doctype)
- html = `` + html;
- return html;
- }
-
private _serveServiceWorker(request: http.IncomingMessage, response: http.ServerResponse): boolean {
function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) {
const pageToResourcesByUrl = new Map();
@@ -261,7 +170,7 @@ export class SnapshotServer {
}
if (request.mode === 'navigate') {
- const htmlResponse = await fetch(`/snapshot-data?pageId=${parsed.pageId}&snapshotId=${parsed.snapshotId}×tamp=${parsed.timestamp}&frameId=${parsed.frameId}`);
+ const htmlResponse = await fetch(`/snapshot-data?pageId=${parsed.pageId}&snapshotId=${parsed.snapshotId || ''}×tamp=${parsed.timestamp || ''}&frameId=${parsed.frameId || ''}`);
const { html, resourcesByUrl, overridenUrls, resourceOverrides } = await htmlResponse.json();
if (!html)
return respondNotAvailable();
@@ -320,8 +229,11 @@ export class SnapshotServer {
response.statusCode = 200;
response.setHeader('Cache-Control', 'public, max-age=31536000');
response.setHeader('Content-Type', 'application/json');
- const parsed = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1));
- const snapshotData = this._frameSnapshotData(parsed as any);
+ const parsed: any = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1));
+ const snapshot = parsed.snapshotId ?
+ this._traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) :
+ this._traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!);
+ const snapshotData: any = snapshot ? snapshot.serialize() : { html: '' };
response.end(JSON.stringify(snapshotData));
return true;
}
@@ -351,7 +263,7 @@ export class SnapshotServer {
return false;
}
- const resource = this._resourceById.get(resourceId);
+ const resource = this._traceModel.resourceById.get(resourceId);
if (!resource)
return false;
const sha1 = overrideSha1 || resource.responseSha1;
@@ -379,31 +291,3 @@ export class SnapshotServer {
}
}
}
-
-const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
-const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
-
-function escapeAttribute(s: string): string {
- return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
-}
-function escapeText(s: string): string {
- return s.replace(/[&<]/ug, char => (escaped as any)[char]);
-}
-
-function snapshotNodes(snapshot: trace.FrameSnapshot): trace.NodeSnapshot[] {
- if (!(snapshot as any)._nodes) {
- const nodes: trace.NodeSnapshot[] = [];
- const visit = (n: trace.NodeSnapshot) => {
- if (typeof n === 'string') {
- nodes.push(n);
- } else if (typeof n[0] === 'string') {
- for (let i = 2; i < n.length; i++)
- visit(n[i]);
- nodes.push(n);
- }
- };
- visit(snapshot.html);
- (snapshot as any)._nodes = nodes;
- }
- return (snapshot as any)._nodes;
-}
diff --git a/src/server/trace/viewer/traceModel.ts b/src/server/trace/viewer/traceModel.ts
index bcdfaa1469..87e2cc8b41 100644
--- a/src/server/trace/viewer/traceModel.ts
+++ b/src/server/trace/viewer/traceModel.ts
@@ -14,16 +14,170 @@
* limitations under the License.
*/
+import { createGuid } from '../../../utils/utils';
import * as trace from '../common/traceEvents';
+import { FrameSnapshot } from './frameSnapshot';
export * as trace from '../common/traceEvents';
-export type TraceModel = {
- contexts: ContextEntry[];
-};
+export class TraceModel {
+ contextEntries = new Map();
+ pageEntries = new Map();
+ resourceById = new Map();
+
+ appendEvents(events: trace.TraceEvent[]) {
+ for (const event of events)
+ this.appendEvent(event);
+ }
+
+ appendEvent(event: trace.TraceEvent) {
+ switch (event.type) {
+ case 'context-created': {
+ this.contextEntries.set(event.contextId, {
+ name: event.debugName || createGuid(),
+ startTime: Number.MAX_VALUE,
+ endTime: Number.MIN_VALUE,
+ created: event,
+ destroyed: undefined as any,
+ pages: [],
+ resourcesByUrl: {},
+ overridenUrls: {}
+ });
+ break;
+ }
+ case 'context-destroyed': {
+ this.contextEntries.get(event.contextId)!.destroyed = event;
+ break;
+ }
+ case 'page-created': {
+ const pageEntry: PageEntry = {
+ created: event,
+ destroyed: undefined as any,
+ actions: [],
+ resources: [],
+ interestingEvents: [],
+ snapshotsByFrameId: {},
+ };
+ const contextEntry = this.contextEntries.get(event.contextId)!;
+ this.pageEntries.set(event.pageId, { pageEntry, contextEntry });
+ contextEntry.pages.push(pageEntry);
+ break;
+ }
+ case 'page-destroyed': {
+ this.pageEntries.get(event.pageId)!.pageEntry.destroyed = event;
+ break;
+ }
+ case 'action': {
+ if (!kInterestingActions.includes(event.method))
+ break;
+ const { pageEntry } = this.pageEntries.get(event.pageId!)!;
+ const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length;
+ const action: ActionEntry = {
+ actionId,
+ action: event,
+ thumbnailUrl: `/action-preview/${actionId}.png`,
+ resources: pageEntry.resources,
+ };
+ pageEntry.resources = [];
+ pageEntry.actions.push(action);
+ break;
+ }
+ case 'resource': {
+ const { pageEntry } = this.pageEntries.get(event.pageId!)!;
+ const action = pageEntry.actions[pageEntry.actions.length - 1];
+ (action || pageEntry).resources.push(event);
+ this.appendResource(event);
+ break;
+ }
+ case 'dialog-opened':
+ case 'dialog-closed':
+ case 'navigation':
+ case 'load': {
+ const { pageEntry } = this.pageEntries.get(event.pageId)!;
+ pageEntry.interestingEvents.push(event);
+ break;
+ }
+ case 'snapshot': {
+ const { pageEntry } = this.pageEntries.get(event.pageId!)!;
+ let snapshots = pageEntry.snapshotsByFrameId[event.frameId];
+ if (!snapshots) {
+ snapshots = [];
+ pageEntry.snapshotsByFrameId[event.frameId] = snapshots;
+ }
+ snapshots.push(event);
+ const contextEntry = this.contextEntries.get(event.contextId)!;
+ for (const override of event.snapshot.resourceOverrides) {
+ if (override.ref) {
+ const refOverride = snapshots[snapshots.length - 1 - override.ref]?.snapshot.resourceOverrides.find(o => o.url === override.url);
+ override.sha1 = refOverride?.sha1;
+ delete override.ref;
+ }
+ contextEntry.overridenUrls[override.url] = true;
+ }
+ break;
+ }
+ }
+ const contextEntry = this.contextEntries.get(event.contextId)!;
+ contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp);
+ contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
+ }
+
+ appendResource(event: trace.NetworkResourceTraceEvent) {
+ const contextEntry = this.contextEntries.get(event.contextId)!;
+ let responseEvents = contextEntry.resourcesByUrl[event.url];
+ if (!responseEvents) {
+ responseEvents = [];
+ contextEntry.resourcesByUrl[event.url] = responseEvents;
+ }
+ responseEvents.push({ frameId: event.frameId, resourceId: event.resourceId });
+ this.resourceById.set(event.resourceId, event);
+ }
+
+ actionById(actionId: string): { context: ContextEntry, page: PageEntry, action: ActionEntry } {
+ const [contextId, pageId, actionIndex] = actionId.split('/');
+ const context = this.contextEntries.get(contextId)!;
+ const page = context.pages.find(entry => entry.created.pageId === pageId)!;
+ const action = page.actions[+actionIndex];
+ return { context, page, action };
+ }
+
+ findPage(pageId: string): { contextEntry: ContextEntry | undefined, pageEntry: PageEntry | undefined } {
+ let contextEntry;
+ let pageEntry;
+ for (const c of this.contextEntries.values()) {
+ for (const p of c.pages) {
+ if (p.created.pageId === pageId) {
+ contextEntry = c;
+ pageEntry = p;
+ }
+ }
+ }
+ return { contextEntry, pageEntry };
+ }
+
+ findSnapshotById(pageId: string, frameId: string, snapshotId: string): FrameSnapshot | undefined {
+ const { pageEntry, contextEntry } = this.pageEntries.get(pageId)!;
+ const frameSnapshots = pageEntry.snapshotsByFrameId[frameId];
+ for (let index = 0; index < frameSnapshots.length; index++) {
+ if (frameSnapshots[index].snapshotId === snapshotId)
+ return new FrameSnapshot(contextEntry, frameSnapshots, index);
+ }
+ }
+
+ findSnapshotByTime(pageId: string, frameId: string, timestamp: number): FrameSnapshot | undefined {
+ const { pageEntry, contextEntry } = this.pageEntries.get(pageId)!;
+ const frameSnapshots = pageEntry.snapshotsByFrameId[frameId];
+ let snapshotIndex = -1;
+ for (let index = 0; index < frameSnapshots.length; index++) {
+ const snapshot = frameSnapshots[index];
+ if (timestamp && snapshot.timestamp <= timestamp)
+ snapshotIndex = index;
+ }
+ return snapshotIndex >= 0 ? new FrameSnapshot(contextEntry, frameSnapshots, snapshotIndex) : undefined;
+ }
+}
export type ContextEntry = {
name: string;
- filePath: string;
startTime: number;
endTime: number;
created: trace.ContextCreatedTraceEvent;
@@ -33,17 +187,11 @@ export type ContextEntry = {
overridenUrls: { [key: string]: boolean };
}
-export type VideoEntry = {
- video: trace.PageVideoTraceEvent;
- videoId: string;
-};
-
export type InterestingPageEvent = trace.DialogOpenedEvent | trace.DialogClosedEvent | trace.NavigationEvent | trace.LoadEvent;
export type PageEntry = {
created: trace.PageCreatedTraceEvent;
destroyed: trace.PageDestroyedTraceEvent;
- video?: VideoEntry;
actions: ActionEntry[];
interestingEvents: InterestingPageEvent[];
resources: trace.NetworkResourceTraceEvent[];
@@ -57,153 +205,4 @@ export type ActionEntry = {
resources: trace.NetworkResourceTraceEvent[];
};
-export type VideoMetaInfo = {
- frames: number;
- width: number;
- height: number;
- fps: number;
- startTime: number;
- endTime: number;
-};
-
const kInterestingActions = ['click', 'dblclick', 'hover', 'check', 'uncheck', 'tap', 'fill', 'press', 'type', 'selectOption', 'setInputFiles', 'goto', 'setContent', 'goBack', 'goForward', 'reload'];
-
-export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) {
- const contextEntries = new Map();
- const pageEntries = new Map();
- for (const event of events) {
- switch (event.type) {
- case 'context-created': {
- contextEntries.set(event.contextId, {
- filePath,
- name: event.debugName || filePath.substring(filePath.lastIndexOf('/') + 1),
- startTime: Number.MAX_VALUE,
- endTime: Number.MIN_VALUE,
- created: event,
- destroyed: undefined as any,
- pages: [],
- resourcesByUrl: {},
- overridenUrls: {}
- });
- break;
- }
- case 'context-destroyed': {
- contextEntries.get(event.contextId)!.destroyed = event;
- break;
- }
- case 'page-created': {
- const pageEntry: PageEntry = {
- created: event,
- destroyed: undefined as any,
- actions: [],
- resources: [],
- interestingEvents: [],
- snapshotsByFrameId: {},
- };
- pageEntries.set(event.pageId, pageEntry);
- contextEntries.get(event.contextId)!.pages.push(pageEntry);
- break;
- }
- case 'page-destroyed': {
- pageEntries.get(event.pageId)!.destroyed = event;
- break;
- }
- case 'page-video': {
- const pageEntry = pageEntries.get(event.pageId)!;
- pageEntry.video = { video: event, videoId: event.contextId + '/' + event.pageId };
- break;
- }
- case 'action': {
- if (!kInterestingActions.includes(event.method))
- break;
- const pageEntry = pageEntries.get(event.pageId!)!;
- const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length;
- const action: ActionEntry = {
- actionId,
- action: event,
- thumbnailUrl: `/action-preview/${actionId}.png`,
- resources: pageEntry.resources,
- };
- pageEntry.resources = [];
- pageEntry.actions.push(action);
- break;
- }
- case 'resource': {
- const pageEntry = pageEntries.get(event.pageId!)!;
- const action = pageEntry.actions[pageEntry.actions.length - 1];
- if (action)
- action.resources.push(event);
- else
- pageEntry.resources.push(event);
- break;
- }
- case 'dialog-opened':
- case 'dialog-closed':
- case 'navigation':
- case 'load': {
- const pageEntry = pageEntries.get(event.pageId)!;
- pageEntry.interestingEvents.push(event);
- break;
- }
- case 'snapshot': {
- const pageEntry = pageEntries.get(event.pageId!)!;
- if (!(event.frameId in pageEntry.snapshotsByFrameId))
- pageEntry.snapshotsByFrameId[event.frameId] = [];
- pageEntry.snapshotsByFrameId[event.frameId]!.push(event);
- break;
- }
- }
-
- const contextEntry = contextEntries.get(event.contextId)!;
- contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp);
- contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
- }
- traceModel.contexts.push(...contextEntries.values());
- preprocessModel(traceModel);
-}
-
-function preprocessModel(traceModel: TraceModel) {
- for (const contextEntry of traceModel.contexts) {
- const appendResource = (event: trace.NetworkResourceTraceEvent) => {
- let responseEvents = contextEntry.resourcesByUrl[event.url];
- if (!responseEvents) {
- responseEvents = [];
- contextEntry.resourcesByUrl[event.url] = responseEvents;
- }
- responseEvents.push({ frameId: event.frameId, resourceId: event.resourceId });
- };
- for (const pageEntry of contextEntry.pages) {
- for (const action of pageEntry.actions)
- action.resources.forEach(appendResource);
- pageEntry.resources.forEach(appendResource);
- for (const snapshots of Object.values(pageEntry.snapshotsByFrameId)) {
- for (let i = 0; i < snapshots.length; ++i) {
- const snapshot = snapshots[i];
- for (const override of snapshot.snapshot.resourceOverrides) {
- if (override.ref) {
- const refOverride = snapshots[i - override.ref]?.snapshot.resourceOverrides.find(o => o.url === override.url);
- override.sha1 = refOverride?.sha1;
- delete override.ref;
- }
- contextEntry.overridenUrls[override.url] = true;
- }
- }
- }
- }
- }
-}
-
-export function actionById(traceModel: TraceModel, actionId: string): { context: ContextEntry, page: PageEntry, action: ActionEntry } {
- const [contextId, pageId, actionIndex] = actionId.split('/');
- const context = traceModel.contexts.find(entry => entry.created.contextId === contextId)!;
- const page = context.pages.find(entry => entry.created.pageId === pageId)!;
- const action = page.actions[+actionIndex];
- return { context, page, action };
-}
-
-export function videoById(traceModel: TraceModel, videoId: string): { context: ContextEntry, page: PageEntry } {
- const [contextId, pageId] = videoId.split('/');
- const context = traceModel.contexts.find(entry => entry.created.contextId === contextId)!;
- const page = context.pages.find(entry => entry.created.pageId === pageId)!;
- return { context, page };
-}
diff --git a/src/server/trace/viewer/traceServer.ts b/src/server/trace/viewer/traceServer.ts
index 33fd25b67b..b171ecfaeb 100644
--- a/src/server/trace/viewer/traceServer.ts
+++ b/src/server/trace/viewer/traceServer.ts
@@ -34,10 +34,10 @@ export class TraceServer {
const traceModelHandler: ServerRouteHandler = (request, response) => {
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
- response.end(JSON.stringify(this._traceModel));
+ response.end(JSON.stringify(Array.from(this._traceModel.contextEntries.values())));
return true;
};
- this.routePath('/tracemodel', traceModelHandler);
+ this.routePath('/contexts', traceModelHandler);
}
routePrefix(prefix: string, handler: ServerRouteHandler, skipReferrerCheck?: boolean) {
diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts
index 96996b70a0..fb7bddccb0 100644
--- a/src/server/trace/viewer/traceViewer.ts
+++ b/src/server/trace/viewer/traceViewer.ts
@@ -19,7 +19,7 @@ import path from 'path';
import * as playwright from '../../../..';
import * as util from 'util';
import { ScreenshotGenerator } from './screenshotGenerator';
-import { readTraceFile, TraceModel } from './traceModel';
+import { TraceModel } from './traceModel';
import type { TraceEvent } from '../common/traceEvents';
import { SnapshotServer } from './snapshotServer';
import { ServerRouteHandler, TraceServer } from './traceServer';
@@ -31,41 +31,14 @@ type TraceViewerDocument = {
model: TraceModel;
};
-const emptyModel: TraceModel = {
- contexts: [
- {
- startTime: 0,
- endTime: 1,
- created: {
- timestamp: Date.now(),
- type: 'context-created',
- browserName: 'none',
- contextId: '',
- deviceScaleFactor: 1,
- isMobile: false,
- viewportSize: { width: 800, height: 600 },
- snapshotScript: '',
- },
- destroyed: {
- timestamp: Date.now(),
- type: 'context-destroyed',
- contextId: '',
- },
- name: '',
- filePath: '',
- pages: [],
- resourcesByUrl: {},
- overridenUrls: {}
- }
- ],
-};
+const emptyModel: TraceModel = new TraceModel();
class TraceViewer {
private _document: TraceViewerDocument | undefined;
async load(traceDir: string) {
const resourcesDir = path.join(traceDir, 'resources');
- const model = { contexts: [] };
+ const model = new TraceModel();
this._document = {
model,
resourcesDir,
@@ -77,7 +50,7 @@ class TraceViewer {
const filePath = path.join(traceDir, name);
const traceContent = await fsReadFileAsync(filePath, 'utf8');
const events = traceContent.split('\n').map(line => line.trim()).filter(line => !!line).map(line => JSON.parse(line)) as TraceEvent[];
- readTraceFile(events, model, filePath);
+ model.appendEvents(events);
}
}
diff --git a/src/web/traceViewer/index.tsx b/src/web/traceViewer/index.tsx
index 22d491124b..6187062542 100644
--- a/src/web/traceViewer/index.tsx
+++ b/src/web/traceViewer/index.tsx
@@ -23,6 +23,6 @@ import '../common.css';
(async () => {
applyTheme();
- const traceModel = await fetch('/tracemodel').then(response => response.json());
- ReactDOM.render(, document.querySelector('#root'));
+ const contexts = await fetch('/contexts').then(response => response.json());
+ ReactDOM.render(, document.querySelector('#root'));
})();
diff --git a/src/web/traceViewer/ui/workbench.tsx b/src/web/traceViewer/ui/workbench.tsx
index 5dcf522656..dcbd44fe4c 100644
--- a/src/web/traceViewer/ui/workbench.tsx
+++ b/src/web/traceViewer/ui/workbench.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import { ActionEntry, TraceModel } from '../../../server/trace/viewer/traceModel';
+import { ActionEntry, ContextEntry, TraceModel } from '../../../server/trace/viewer/traceModel';
import { ActionList } from './actionList';
import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline';
@@ -27,9 +27,9 @@ import { SnapshotTab } from './snapshotTab';
import { LogsTab } from './logsTab';
export const Workbench: React.FunctionComponent<{
- traceModel: TraceModel,
-}> = ({ traceModel }) => {
- const [context, setContext] = React.useState(traceModel.contexts[0]);
+ contexts: ContextEntry[],
+}> = ({ contexts }) => {
+ const [context, setContext] = React.useState(contexts[0]);
const [selectedAction, setSelectedAction] = React.useState();
const [highlightedAction, setHighlightedAction] = React.useState();
const [selectedTime, setSelectedTime] = React.useState();
@@ -51,7 +51,7 @@ export const Workbench: React.FunctionComponent<{
Playwright
{
setContext(context);
diff --git a/utils/check_deps.js b/utils/check_deps.js
index 8f0d093199..0cc7030981 100644
--- a/utils/check_deps.js
+++ b/utils/check_deps.js
@@ -159,7 +159,7 @@ DEPS['src/utils/'] = ['src/common/'];
// Trace viewer
DEPS['src/server/trace/recorder/'] = ['src/server/trace/common/', ...DEPS['src/server/']];
-DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/'];
+DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/', ...DEPS['src/server/']];
checkDeps().catch(e => {
console.error(e && e.stack ? e.stack : e);