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