chore: make trace model a class (#5600)

This commit is contained in:
Pavel Feldman 2021-02-24 14:22:34 -08:00 committed by GitHub
parent f71bf9a42a
commit 6bf3fe8432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 301 additions and 325 deletions

View File

@ -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 = `<!DOCTYPE ${snapshot.doctype}>` + html;
html += `<script>${this.contextEntry.created.snapshotScript}</script>`;
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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' };
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;
}

View File

@ -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<Buffer | undefined> {
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);

View File

@ -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<string, trace.NetworkResourceTraceEvent>;
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 += `<script>${contextEntry.created.snapshotScript}</script>`;
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 = `<!DOCTYPE ${snapshot.doctype}>` + html;
return html;
}
private _serveServiceWorker(request: http.IncomingMessage, response: http.ServerResponse): boolean {
function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) {
const pageToResourcesByUrl = new Map<string, { [key: string]: { resourceId: string, frameId: string }[] }>();
@ -261,7 +170,7 @@ export class SnapshotServer {
}
if (request.mode === 'navigate') {
const htmlResponse = await fetch(`/snapshot-data?pageId=${parsed.pageId}&snapshotId=${parsed.snapshotId}&timestamp=${parsed.timestamp}&frameId=${parsed.frameId}`);
const htmlResponse = await fetch(`/snapshot-data?pageId=${parsed.pageId}&snapshotId=${parsed.snapshotId || ''}&timestamp=${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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', '\'': '&#39;' };
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;
}

View File

@ -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<string, ContextEntry>();
pageEntries = new Map<string, { contextEntry: ContextEntry, pageEntry: PageEntry }>();
resourceById = new Map<string, trace.NetworkResourceTraceEvent>();
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<string, ContextEntry>();
const pageEntries = new Map<string, PageEntry>();
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 };
}

View File

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

View File

@ -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: '<empty>',
deviceScaleFactor: 1,
isMobile: false,
viewportSize: { width: 800, height: 600 },
snapshotScript: '',
},
destroyed: {
timestamp: Date.now(),
type: 'context-destroyed',
contextId: '<empty>',
},
name: '<empty>',
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);
}
}

View File

@ -23,6 +23,6 @@ import '../common.css';
(async () => {
applyTheme();
const traceModel = await fetch('/tracemodel').then(response => response.json());
ReactDOM.render(<Workbench traceModel={traceModel} />, document.querySelector('#root'));
const contexts = await fetch('/contexts').then(response => response.json());
ReactDOM.render(<Workbench contexts={contexts} />, document.querySelector('#root'));
})();

View File

@ -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<ActionEntry | undefined>();
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>();
const [selectedTime, setSelectedTime] = React.useState<number | undefined>();
@ -51,7 +51,7 @@ export const Workbench: React.FunctionComponent<{
<div className='product'>Playwright</div>
<div className='spacer'></div>
<ContextSelector
contexts={traceModel.contexts}
contexts={contexts}
context={context}
onChange={context => {
setContext(context);

View File

@ -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);