diff --git a/packages/isomorphic/trace/entries.ts b/packages/isomorphic/trace/entries.ts index a762d285bdd54..d0d3b4a2a50de 100644 --- a/packages/isomorphic/trace/entries.ts +++ b/packages/isomorphic/trace/entries.ts @@ -44,10 +44,38 @@ export type ContextEntry = { stdio: trace.StdioTraceEvent[]; errors: trace.ErrorTraceEvent[]; hasSource: boolean; + hasStepData: boolean; testTimeout?: number; annotations?: trace.TraceEventAnnotation[]; }; +export function createEmptyContext(): ContextEntry { + return { + origin: 'testRunner', + startTime: Number.MAX_SAFE_INTEGER, + wallTime: Number.MAX_SAFE_INTEGER, + monotonicTime: 0, + endTime: 0, + browserName: '', + options: { + deviceScaleFactor: 1, + isMobile: false, + viewport: { width: 1280, height: 800 }, + }, + pages: [], + resources: [], + actions: [], + screenshots: [], + ariaSnapshots: [], + videos: [], + events: [], + errors: [], + stdio: [], + hasSource: false, + hasStepData: false, + }; +} + export type PageEntry = { pageId: string, screencastFrames: { diff --git a/packages/isomorphic/trace/traceLoader.ts b/packages/isomorphic/trace/traceLoader.ts index 9daa159473037..874774ddf6778 100644 --- a/packages/isomorphic/trace/traceLoader.ts +++ b/packages/isomorphic/trace/traceLoader.ts @@ -16,10 +16,12 @@ import { parseClientSideCallMetadata } from './traceUtils'; +import { createEmptyContext } from './entries'; import { SnapshotStorage } from './snapshotStorage'; import { TraceModernizer } from './traceModernizer'; -import type { ContextEntry } from './entries'; +import type { ActionEntry, ContextEntry } from './entries'; +import type { StackFrame } from '@trace/trace'; export interface TraceLoaderBackend { entryNames(): Promise; @@ -30,7 +32,7 @@ export interface TraceLoaderBackend { } export class TraceLoader { - contextEntries: ContextEntry[] = []; + contextEntry: ContextEntry = createEmptyContext(); private _snapshotStorage: SnapshotStorage | undefined; private _backend!: TraceLoaderBackend; private _resourceToContentType = new Map(); @@ -42,34 +44,37 @@ export class TraceLoader { this._backend = backend; const prefix = traceFile?.match(/(.+)\.trace$/)?.[1]; - const prefixes: string[] = []; + const traceNames: string[] = []; + const networkNames: string[] = []; + const stacksNames: string[] = []; let hasSource = false; for (const entryName of await this._backend.entryNames()) { - const match = entryName.match(/(.+)\.trace$/); - if (match && (!prefix || prefix === match[1])) - prefixes.push(match[1] || ''); + if (entryName.endsWith('.trace') && (!prefix || entryName === prefix + '.trace')) + traceNames.push(entryName); + if (entryName.endsWith('.network') && (!prefix || entryName === prefix + '.network')) + networkNames.push(entryName); + if (entryName.endsWith('.stacks') && (!prefix || entryName === prefix + '.stacks')) + stacksNames.push(entryName); if (entryName.startsWith('src/') || entryName.includes('src@')) hasSource = true; } - if (!prefixes.length) + if (!traceNames.length) throw new Error('Cannot find .trace file'); this._snapshotStorage = new SnapshotStorage(); - // 3 * ordinals progress increments below. - const total = prefixes.length * 3; + const total = traceNames.length + networkNames.length + stacksNames.length; let done = 0; - for (const prefix of prefixes) { + const contextEntries: ContextEntry[] = []; + + // Load trace files first to learn the trace format version, because network files do not include one. + let version: number | undefined; + for (const traceName of traceNames) { const contextEntry = createEmptyContext(); contextEntry.hasSource = hasSource; const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage); - - const trace = await this._backend.readText(prefix + '.trace') || ''; - modernizer.appendTrace(trace); - unzipProgress?.(++done, total); - - const network = await this._backend.readText(prefix + '.network') || ''; - modernizer.appendTrace(network); + modernizer.appendTrace(await this._backend.readText(traceName) || ''); + version = Math.min(version ?? Number.MAX_SAFE_INTEGER, modernizer.version() ?? Number.MAX_SAFE_INTEGER); unzipProgress?.(++done, total); contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); @@ -88,24 +93,53 @@ export class TraceLoader { } } - const stacks = await this._backend.readText(prefix + '.stacks'); + contextEntries.push(contextEntry); + } + + for (const networkName of networkNames) { + const contextEntry = createEmptyContext(); + contextEntry.origin = 'library'; + const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage, version === Number.MAX_SAFE_INTEGER ? undefined : version); + modernizer.appendTrace(await this._backend.readText(networkName) || ''); + unzipProgress?.(++done, total); + + // Network files do not include the time origin event, + // so we derive the time origin from the resources instead. + for (const resource of contextEntry.resources) { + // eslint-disable-next-line no-restricted-globals + const wallTime = resource.startedDateTime ? Date.parse(resource.startedDateTime) : NaN; + if (resource._monotonicTime && !isNaN(wallTime)) { + contextEntry.wallTime = wallTime; + contextEntry.monotonicTime = resource._monotonicTime; + break; + } + } + + contextEntries.push(contextEntry); + } + + const callMetadata = new Map(); + for (const stacksName of stacksNames) { + const stacks = await this._backend.readText(stacksName); if (stacks) { - const callMetadata = parseClientSideCallMetadata(JSON.parse(stacks)); - for (const action of contextEntry.actions) - action.stack = action.stack || callMetadata.get(action.callId); + for (const [callId, stack] of parseClientSideCallMetadata(JSON.parse(stacks))) + callMetadata.set(callId, stack); } unzipProgress?.(++done, total); + } + for (const contextEntry of contextEntries) { for (const resource of contextEntry.resources) { if (resource.request.postData?._file) this._resourceToContentType.set(resource.request.postData._file, stripEncodingFromContentType(resource.request.postData.mimeType)); if (resource.response.content?._file) this._resourceToContentType.set(resource.response.content._file, stripEncodingFromContentType(resource.response.content.mimeType)); } - - this.contextEntries.push(contextEntry); } + this.contextEntry = mergeContextEntries(contextEntries); + for (const action of this.contextEntry.actions) + action.stack = action.stack || callMetadata.get(action.callId); this._snapshotStorage.finalize(); } @@ -134,28 +168,124 @@ function stripEncodingFromContentType(contentType: string) { return contentType; } -function createEmptyContext(): ContextEntry { - return { - origin: 'testRunner', - startTime: Number.MAX_SAFE_INTEGER, - wallTime: Number.MAX_SAFE_INTEGER, - monotonicTime: 0, - endTime: 0, - browserName: '', - options: { - deviceScaleFactor: 1, - isMobile: false, - viewport: { width: 1280, height: 800 }, - }, - pages: [], - resources: [], - actions: [], - screenshots: [], - ariaSnapshots: [], - videos: [], - events: [], - errors: [], - stdio: [], - hasSource: false, - }; +function mergeContextEntries(entries: ContextEntry[]): ContextEntry { + const libraryEntries = entries.filter(entry => entry.origin === 'library'); + const testRunnerEntries = entries.filter(entry => entry.origin === 'testRunner'); + + // Align each file with the test runner clock. This updates all the timestamps, + // so it must be done before merging events below. + const timeOrigin = (entry: ContextEntry) => entry.wallTime - entry.monotonicTime; + const runnerEntry = testRunnerEntries.find(entry => entry.monotonicTime); + for (const entry of libraryEntries) { + if (runnerEntry && entry.monotonicTime) + adjustMonotonicTime(entry, timeOrigin(entry) - timeOrigin(runnerEntry)); + } + + const libraryEntry = libraryEntries[0]; + const testRunnerEntry = testRunnerEntries[0]; + const result = createEmptyContext(); + result.origin = libraryEntry ? 'library' : 'testRunner'; + result.browserName = libraryEntry?.browserName || ''; + result.channel = libraryEntry?.channel; + result.platform = libraryEntry?.platform; + result.playwrightVersion = entries.find(entry => entry.playwrightVersion)?.playwrightVersion; + result.sdkLanguage = libraryEntry?.sdkLanguage; + result.testIdAttributeName = libraryEntry?.testIdAttributeName; + result.title = libraryEntry?.title; + result.options = libraryEntry?.options || {}; + result.testTimeout = testRunnerEntry?.testTimeout; + result.annotations = testRunnerEntry?.annotations; + result.hasSource = entries.some(entry => entry.hasSource); + result.hasStepData = !!testRunnerEntry; + result.wallTime = entries.reduce((prev, entry) => Math.min(prev, entry.wallTime), result.wallTime); + result.startTime = entries.reduce((prev, entry) => Math.min(prev, entry.startTime), result.startTime); + result.endTime = entries.reduce((prev, entry) => Math.max(prev, entry.endTime), result.endTime); + result.pages = entries.flatMap(entry => entry.pages); + result.resources = entries.flatMap(entry => entry.resources); + result.actions = mergeActions(entries); + result.screenshots = entries.flatMap(entry => entry.screenshots); + result.ariaSnapshots = entries.flatMap(entry => entry.ariaSnapshots); + result.videos = entries.flatMap(entry => entry.videos); + result.events = entries.flatMap(entry => entry.events); + result.stdio = entries.flatMap(entry => entry.stdio); + result.errors = entries.flatMap(entry => entry.errors); + return result; +} + +let lastTmpStepId = 0; + +function mergeActions(entries: ContextEntry[]): ActionEntry[] { + const libraryEntries = entries.filter(entry => entry.origin === 'library'); + const testRunnerEntries = entries.filter(entry => entry.origin === 'testRunner'); + + // With library-only or test-runner-only traces there is nothing to match. + if (!testRunnerEntries.length || !libraryEntries.length) + return entries.flatMap(entry => entry.actions.map(action => ({ ...action }))); + + const map = new Map(); + for (const entry of libraryEntries) { + for (const action of entry.actions) { + // Never merge stepless events. + map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); + } + } + + const nonPrimaryIdToPrimaryId = new Map(); + for (const entry of testRunnerEntries) { + for (const action of entry.actions) { + const existing = action.stepId && map.get(action.stepId); + if (existing) { + nonPrimaryIdToPrimaryId.set(action.callId, existing.callId); + if (action.error) + existing.error = action.error; + if (action.attachments) + existing.attachments = action.attachments; + if (action.annotations) + existing.annotations = action.annotations; + if (action.parentId) + existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; + if (action.group) + existing.group = action.group; + // For the events that are present in the test runner context, always take + // their time from the test runner context to preserve client side order. + existing.startTime = action.startTime; + existing.endTime = action.endTime; + continue; + } + if (action.parentId) + action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; + map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); + } + } + return [...map.values()]; +} + +function adjustMonotonicTime(entry: ContextEntry, monotonicTimeDelta: number) { + if (!monotonicTimeDelta) + return; + if (entry.startTime !== Number.MAX_SAFE_INTEGER) + entry.startTime += monotonicTimeDelta; + if (entry.endTime) + entry.endTime += monotonicTimeDelta; + entry.monotonicTime += monotonicTimeDelta; + for (const action of entry.actions) { + if (action.startTime) + action.startTime += monotonicTimeDelta; + if (action.endTime) + action.endTime += monotonicTimeDelta; + } + for (const event of entry.events) + event.time += monotonicTimeDelta; + for (const event of entry.stdio) + event.timestamp += monotonicTimeDelta; + for (const page of entry.pages) { + for (const frame of page.screencastFrames) + frame.timestamp += monotonicTimeDelta; + } + for (const video of entry.videos) + video.timestampOrigin += monotonicTimeDelta; + for (const resource of entry.resources) { + if (resource._monotonicTime) + resource._monotonicTime += monotonicTimeDelta; + } } diff --git a/packages/isomorphic/trace/traceModel.ts b/packages/isomorphic/trace/traceModel.ts index eef67a59a6cec..584f505e88a44 100644 --- a/packages/isomorphic/trace/traceModel.ts +++ b/packages/isomorphic/trace/traceModel.ts @@ -93,44 +93,34 @@ export class TraceModel { private _screenshots = new Map(); private _ariaSnapshots = new Map(); - constructor(traceUri: string, contexts: ContextEntry[]) { - const libraryContext = contexts.find(context => context.origin === 'library'); - + constructor(traceUri: string, contextEntry: ContextEntry) { this.traceUri = traceUri; - this.browserName = libraryContext?.browserName || ''; - this.sdkLanguage = libraryContext?.sdkLanguage; - this.channel = libraryContext?.channel; - this.testIdAttributeName = libraryContext?.testIdAttributeName; - this.platform = libraryContext?.platform || ''; - this.playwrightVersion = contexts.find(c => c.playwrightVersion)?.playwrightVersion; - this.title = libraryContext?.title || ''; - this.options = libraryContext?.options || {}; - this.testTimeout = contexts.find(c => c.origin === 'testRunner')?.testTimeout; - this.annotations = contexts.find(c => c.origin === 'testRunner')?.annotations; - // Next call updates all timestamps for all events in library contexts, so it must be done first. - this.actions = mergeActionsAndUpdateTiming(contexts); - this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages)); - this.videos = []; - this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE); - this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE); - this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE); - this.events = ([] as (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]).concat(...contexts.map(c => c.events)); - this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio)); - this.errors = ([] as trace.ErrorTraceEvent[]).concat(...contexts.map(c => c.errors)); - this.hasSource = contexts.some(c => c.hasSource); - this.hasStepData = contexts.some(context => context.origin === 'testRunner'); - this.resources = []; - for (let i = 0; i < contexts.length; ++i) { - for (const entry of contexts[i].resources) - this.resources.push({ ...entry, id: `${resourceOwnerRef(entry) ?? i}-${entry.startedDateTime}-${entry.request.url}` }); - } - for (const context of contexts) { - for (const event of context.screenshots || []) - this._screenshots.set(`${event.callId}/${event.phase}`, event); - for (const event of context.ariaSnapshots || []) - this._ariaSnapshots.set(`${event.callId}/${event.phase}`, event); - this.videos.push(...(context.videos || [])); - } + this.browserName = contextEntry.browserName; + this.sdkLanguage = contextEntry.sdkLanguage; + this.channel = contextEntry.channel; + this.testIdAttributeName = contextEntry.testIdAttributeName; + this.platform = contextEntry.platform || ''; + this.playwrightVersion = contextEntry.playwrightVersion; + this.title = contextEntry.title || ''; + this.options = contextEntry.options; + this.testTimeout = contextEntry.testTimeout; + this.annotations = contextEntry.annotations; + this.actions = sortAndLinkActions(contextEntry.actions); + this.pages = contextEntry.pages; + this.videos = contextEntry.videos; + this.wallTime = contextEntry.wallTime; + this.startTime = contextEntry.startTime; + this.endTime = contextEntry.endTime; + this.events = contextEntry.events; + this.stdio = contextEntry.stdio; + this.errors = contextEntry.errors; + this.hasSource = contextEntry.hasSource; + this.hasStepData = contextEntry.hasStepData; + this.resources = contextEntry.resources.map((entry, index) => ({ ...entry, id: `${resourceOwnerRef(entry) ?? index}-${entry.startedDateTime}-${entry.request.url}` })); + for (const event of contextEntry.screenshots) + this._screenshots.set(`${event.callId}/${event.phase}`, event); + for (const event of contextEntry.ariaSnapshots) + this._ariaSnapshots.set(`${event.callId}/${event.phase}`, event); this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUri })) ?? []); this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_')); @@ -250,8 +240,8 @@ export class TraceModel { } } -function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { - const result = mergeActionsAndUpdateTimingSameTrace(contexts); +function sortAndLinkActions(actions: ActionEntry[]) { + const result = actions.slice(); result.sort((a1, a2) => { if (a2.parentId === a1.callId) @@ -278,93 +268,6 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { return result; } -let lastTmpStepId = 0; - -function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionEntry[] { - const map = new Map(); - - const libraryContexts = contexts.filter(context => context.origin === 'library'); - const testRunnerContexts = contexts.filter(context => context.origin === 'testRunner'); - - // With library-only or test-runner-only traces there is nothing to match. - if (!testRunnerContexts.length || !libraryContexts.length) { - return contexts.map(context => { - return context.actions.map(action => ({ ...action })); - }).flat(); - } - - const timeOrigin = (context: ContextEntry) => context.wallTime - context.monotonicTime; - const runnerContext = testRunnerContexts.find(context => context.monotonicTime); - for (const context of libraryContexts) { - if (runnerContext && context.monotonicTime) - adjustMonotonicTime(context, timeOrigin(context) - timeOrigin(runnerContext)); - } - - for (const context of libraryContexts) { - for (const action of context.actions) { - // Never merge stepless events. - map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); - } - } - - const nonPrimaryIdToPrimaryId = new Map(); - for (const context of testRunnerContexts) { - for (const action of context.actions) { - const existing = action.stepId && map.get(action.stepId); - if (existing) { - nonPrimaryIdToPrimaryId.set(action.callId, existing.callId); - if (action.error) - existing.error = action.error; - if (action.attachments) - existing.attachments = action.attachments; - if (action.annotations) - existing.annotations = action.annotations; - if (action.parentId) - existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; - if (action.group) - existing.group = action.group; - // For the events that are present in the test runner context, always take - // their time from the test runner context to preserve client side order. - existing.startTime = action.startTime; - existing.endTime = action.endTime; - continue; - } - if (action.parentId) - action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId; - map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action }); - } - } - return [...map.values()]; -} - -function adjustMonotonicTime(context: ContextEntry, monotonicTimeDelta: number) { - if (!monotonicTimeDelta) - return; - context.startTime += monotonicTimeDelta; - context.endTime += monotonicTimeDelta; - context.monotonicTime += monotonicTimeDelta; - for (const action of context.actions) { - if (action.startTime) - action.startTime += monotonicTimeDelta; - if (action.endTime) - action.endTime += monotonicTimeDelta; - } - for (const event of context.events) - event.time += monotonicTimeDelta; - for (const event of context.stdio) - event.timestamp += monotonicTimeDelta; - for (const page of context.pages) { - for (const frame of page.screencastFrames) - frame.timestamp += monotonicTimeDelta; - } - for (const video of context.videos || []) - video.timestampOrigin += monotonicTimeDelta; - for (const resource of context.resources) { - if (resource._monotonicTime) - resource._monotonicTime += monotonicTimeDelta; - } -} - export function buildActionTree(actions: ActionEntry[]): { rootItem: ActionTreeItem, itemMap: Map } { const itemMap = new Map(); diff --git a/packages/isomorphic/trace/traceModernizer.ts b/packages/isomorphic/trace/traceModernizer.ts index 20047ddb4d73b..c0c282d75c3a5 100644 --- a/packages/isomorphic/trace/traceModernizer.ts +++ b/packages/isomorphic/trace/traceModernizer.ts @@ -49,9 +49,10 @@ export class TraceModernizer { private _consoleObjects = new Map(); private _apiRequestRef: string | undefined; - constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage) { + constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage, version?: number) { this._contextEntry = contextEntry; this._snapshotStorage = snapshotStorage; + this._version = version; } appendTrace(trace: string) { @@ -59,6 +60,10 @@ export class TraceModernizer { this._appendEvent(line); } + version(): number | undefined { + return this._version; + } + actions(): ActionEntry[] { return [...this._actionMap.values()]; } diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 275a09951f377..ff5e2b71c0d62 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -330,6 +330,10 @@ function traceDescriptor(traceDir: string, tracePrefix: string | undefined) { }; for (const name of fs.readdirSync(traceDir)) { + // Skip per-chunk copies of the network file made for zipping, + // they duplicate the main network file. + if (/-pwnetcopy-\d+\.network$/.test(name)) + continue; if (!tracePrefix || name.startsWith(tracePrefix)) result.entries.push({ name, path: toFilePathUrl(path.join(traceDir, name)) }); } diff --git a/packages/playwright-core/src/tools/trace/traceUtils.ts b/packages/playwright-core/src/tools/trace/traceUtils.ts index 4a93dd3f121bd..7170143d7f1fe 100644 --- a/packages/playwright-core/src/tools/trace/traceUtils.ts +++ b/packages/playwright-core/src/tools/trace/traceUtils.ts @@ -90,7 +90,7 @@ export async function loadTrace(): Promise { const backend = new DirTraceLoaderBackend(traceDir); const loader = new TraceLoader(); await loader.load(backend, traceFile); - const model = new TraceModel(traceDir, loader.contextEntries); + const model = new TraceModel(traceDir, loader.contextEntry); return new LoadedTrace(model, loader, buildOrdinalMap(model)); } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 79d1bc4911923..eb355bdfa69d3 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -205,7 +205,7 @@ async function doFetch(event: FetchEvent): Promise { return errorResponse; if (relativePath === '/contexts') { - return new Response(JSON.stringify(loadedTrace!.traceLoader.contextEntries), { + return new Response(JSON.stringify(loadedTrace!.traceLoader.contextEntry), { status: 200, headers: { 'Content-Type': 'application/json' } }); diff --git a/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx index 10e98eb7a16c9..b38adc66abaf5 100644 --- a/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/liveWorkbenchLoader.tsx @@ -18,6 +18,7 @@ import * as React from 'react'; import { TraceModel } from '@isomorphic/trace/traceModel'; import './workbenchLoader.css'; import { Workbench } from './workbench'; +import { createEmptyContext } from '@isomorphic/trace/entries'; import type { ContextEntry } from '@isomorphic/trace/entries'; @@ -36,7 +37,7 @@ export const LiveWorkbenchLoader: React.FC<{ traceJson: string }> = ({ traceJson const model = await loadSingleTraceFile(traceJson); setModel(model); } catch { - const model = new TraceModel('', []); + const model = new TraceModel('', createEmptyContext()); setModel(model); } finally { setCounter(counter + 1); @@ -55,6 +56,6 @@ async function loadSingleTraceFile(traceJson: string): Promise { const params = new URLSearchParams(); params.set('trace', traceJson); const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - return new TraceModel(traceJson, contextEntries); + const contextEntry = await response.json() as ContextEntry; + return new TraceModel(traceJson, contextEntry); } diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index 30b70a84cba44..d5d9ed7cb1bbb 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -20,6 +20,8 @@ import '@web/common.css'; import '@web/third_party/vscode/codicon.css'; import type * as reporterTypes from 'playwright/types/testReporter'; import React from 'react'; +import { createEmptyContext } from '@isomorphic/trace/entries'; + import type { ContextEntry } from '@isomorphic/trace/entries'; import type { SourceLocation } from '@isomorphic/trace/traceModel'; import { TraceModel } from '@isomorphic/trace/traceModel'; @@ -75,7 +77,7 @@ export const TraceView: React.FC<{ const model = await loadSingleTraceFile(traceLocation, Date.now()); setModel({ model, isLive: true }); } catch { - const model = new TraceModel('', []); + const model = new TraceModel('', createEmptyContext()); model.errorDescriptors.push(...result.errors.flatMap(error => !!error.message ? [{ message: error.message }] : [])); setModel({ model, isLive: false }); } finally { @@ -115,6 +117,6 @@ async function loadSingleTraceFile(absolutePath: string, timestamp: number): Pro const params = new URLSearchParams(); params.set('trace', traceUri); const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - return new TraceModel(traceUri, contextEntries); + const contextEntry = await response.json() as ContextEntry; + return new TraceModel(traceUri, contextEntry); } diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index e3718e63368b5..427935a3ac7be 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -15,6 +15,7 @@ */ import * as React from 'react'; +import { createEmptyContext } from '@isomorphic/trace/entries'; import { TraceModel } from '@isomorphic/trace/traceModel'; import './workbenchLoader.css'; import { Workbench } from './workbench'; @@ -134,8 +135,8 @@ export const WorkbenchLoader: React.FunctionComponent<{ setProcessingErrorMessage(error); return error; } - const contextEntries = await response.json(); - const model = new TraceModel(traceURL, contextEntries); + const contextEntry = await response.json(); + const model = new TraceModel(traceURL, contextEntry); setProgress({ done: 0, total: 0 }); setProcessingErrorMessage(null); setModel(model); @@ -245,4 +246,4 @@ export const WorkbenchLoader: React.FunctionComponent<{ ; }; -export const emptyModel = new TraceModel('', []); +export const emptyModel = new TraceModel('', createEmptyContext()); diff --git a/tests/config/utils.ts b/tests/config/utils.ts index eca03a8006092..4d16f16831981 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -171,7 +171,7 @@ export async function parseTrace(file: string): Promise<{ snapshots: SnapshotSto const backend = new tools.DirTraceLoaderBackend(dir); const loader = new TraceLoader(); await loader.load(backend); - return { model: new TraceModel(dir, loader.contextEntries), snapshots: loader.storage() }; + return { model: new TraceModel(dir, loader.contextEntry), snapshots: loader.storage() }; } export async function parseHar(file: string): Promise> {