diff --git a/src/cli/traceViewer/traceModel.ts b/src/cli/traceViewer/traceModel.ts index 2051ebce9c0b3..eb7da43c82d8b 100644 --- a/src/cli/traceViewer/traceModel.ts +++ b/src/cli/traceViewer/traceModel.ts @@ -15,6 +15,7 @@ */ import * as trace from '../../trace/traceTypes'; +export * as trace from '../../trace/traceTypes'; export type TraceModel = { contexts: ContextEntry[]; @@ -36,11 +37,14 @@ export type VideoEntry = { 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[]; } @@ -88,6 +92,7 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel destroyed: undefined as any, actions: [], resources: [], + interestingEvents: [], }; pageEntries.set(event.pageId, pageEntry); contextEntries.get(event.contextId)!.pages.push(pageEntry); @@ -129,11 +134,19 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel responseEvents.push(event); break; } + case 'dialog-opened': + case 'dialog-closed': + case 'navigation': + case 'load': { + const pageEntry = pageEntries.get(event.pageId)!; + pageEntry.interestingEvents.push(event); + break; + } } const contextEntry = contextEntries.get(event.contextId)!; - contextEntry.startTime = Math.min(contextEntry.startTime, (event as any).timestamp); - contextEntry.endTime = Math.max(contextEntry.endTime, (event as any).timestamp); + contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp); + contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp); } traceModel.contexts.push(...contextEntries.values()); } diff --git a/src/cli/traceViewer/videoTileGenerator.ts b/src/cli/traceViewer/videoTileGenerator.ts index 146c048712ec5..d5eaff890b4fc 100644 --- a/src/cli/traceViewer/videoTileGenerator.ts +++ b/src/cli/traceViewer/videoTileGenerator.ts @@ -81,7 +81,7 @@ function parseMetaInfo(text: string, video: PageVideoTraceEvent): VideoMetaInfo width: parseInt(resolutionMatch![1], 10), height: parseInt(resolutionMatch![2], 10), fps: parseInt(fpsMatch![1], 10), - startTime: (video as any).timestamp, - endTime: (video as any).timestamp + duration + startTime: video.timestamp, + endTime: video.timestamp + duration }; } diff --git a/src/cli/traceViewer/web/common.css b/src/cli/traceViewer/web/common.css index b5b20b52a0e50..60b0c64738698 100644 --- a/src/cli/traceViewer/web/common.css +++ b/src/cli/traceViewer/web/common.css @@ -24,6 +24,7 @@ --purple: #9C27B0; --yellow: #FFC107; --blue: #2196F3; + --transparent-blue: #2196F355; --orange: #d24726; --black: #1E1E1E; --gray: #888888; diff --git a/src/cli/traceViewer/web/index.tsx b/src/cli/traceViewer/web/index.tsx index b0f7b89aa217c..a07b9f53c52a8 100644 --- a/src/cli/traceViewer/web/index.tsx +++ b/src/cli/traceViewer/web/index.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import { TraceModel, VideoMetaInfo } from '../traceModel'; +import { TraceModel, VideoMetaInfo, trace } from '../traceModel'; import './common.css'; import './third_party/vscode/codicon.css'; import { Workbench } from './ui/workbench'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { ActionTraceEvent } from '../../../trace/traceTypes'; declare global { interface Window { getTraceModel(): Promise; getVideoMetaInfo(videoId: string): Promise; readFile(filePath: string): Promise; - renderSnapshot(action: ActionTraceEvent): void; + renderSnapshot(action: trace.ActionTraceEvent): void; } } diff --git a/src/cli/traceViewer/web/ui/timeline.css b/src/cli/traceViewer/web/ui/timeline.css index 497f0a26759b7..3e0e7bc267ca0 100644 --- a/src/cli/traceViewer/web/ui/timeline.css +++ b/src/cli/traceViewer/web/ui/timeline.css @@ -33,7 +33,7 @@ background-color: rgb(0 0 0 / 10%); } -.timeline-label { +.timeline-time { position: absolute; top: 4px; right: 3px; @@ -58,16 +58,16 @@ left: 0; } -.timeline-lane.timeline-action-labels { +.timeline-lane.timeline-labels { margin-top: 10px; } -.timeline-lane.timeline-actions { +.timeline-lane.timeline-bars { margin-bottom: 10px; overflow: visible; } -.timeline-action { +.timeline-bar { position: absolute; top: 0; bottom: 0; @@ -77,25 +77,37 @@ background-color: var(--action-color); } -.timeline-action.selected { +.timeline-bar.selected { filter: brightness(70%); box-shadow: 0 0 0 1px var(--action-color); } -.timeline-action.click { +.timeline-bar.click { --action-color: var(--green); } -.timeline-action.fill, -.timeline-action.press { +.timeline-bar.fill, +.timeline-bar.press { --action-color: var(--orange); } -.timeline-action.goto { +.timeline-bar.goto { --action-color: var(--blue); } -.timeline-action-label { +.timeline-bar.dialog { + --action-color: var(--transparent-blue); +} + +.timeline-bar.navigation { + --action-color: var(--purple); +} + +.timeline-bar.load { + --action-color: var(--yellow); +} + +.timeline-label { position: absolute; top: 0; bottom: 0; @@ -103,13 +115,14 @@ background-color: #fffffff0; justify-content: center; display: none; + white-space: nowrap; } -.timeline-action-label.selected { +.timeline-label.selected { display: flex; } -.timeline-time-bar { +.timeline-marker { display: none; position: absolute; top: 0; @@ -119,6 +132,6 @@ pointer-events: none; } -.timeline-time-bar.timeline-time-bar-hover { +.timeline-marker.timeline-marker-hover { background-color: var(--light-pink); } diff --git a/src/cli/traceViewer/web/ui/timeline.tsx b/src/cli/traceViewer/web/ui/timeline.tsx index 676834fa3c401..10552f4cd8a4d 100644 --- a/src/cli/traceViewer/web/ui/timeline.tsx +++ b/src/cli/traceViewer/web/ui/timeline.tsx @@ -15,13 +15,24 @@ limitations under the License. */ -import { ContextEntry } from '../../traceModel'; +import { ContextEntry, InterestingPageEvent, ActionEntry, trace } from '../../traceModel'; import './timeline.css'; import { FilmStrip } from './filmStrip'; import { Boundaries } from '../geometry'; import * as React from 'react'; import { useMeasure } from './helpers'; -import { ActionEntry } from '../../traceModel'; + +type TimelineBar = { + entry?: ActionEntry; + event?: InterestingPageEvent; + leftPosition: number; + rightPosition: number; + leftTime: number; + rightTime: number; + type: string; + label: string; + priority: number; +}; export const Timeline: React.FunctionComponent<{ context: ContextEntry, @@ -33,51 +44,102 @@ export const Timeline: React.FunctionComponent<{ }> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => { const [measure, ref] = useMeasure(); const [previewX, setPreviewX] = React.useState(); - const targetAction = highlightedAction || selectedAction; + const [hoveredBar, setHoveredBar] = React.useState(); const offsets = React.useMemo(() => { return calculateDividerOffsets(measure.width, boundaries); }, [measure.width, boundaries]); - const actionEntries = React.useMemo(() => { - const actions: ActionEntry[] = []; - for (const page of context.pages) - actions.push(...page.actions); - return actions; - }, [context]); - const actionTimes = React.useMemo(() => { - return actionEntries.map(entry => { - return { - entry, - left: timeToPercent(measure.width, boundaries, entry.action.startTime!), - right: timeToPercent(measure.width, boundaries, entry.action.endTime!), - }; - }); - }, [actionEntries, boundaries, measure.width]); - - const findHoveredAction = (x: number) => { + + let targetBar: TimelineBar | undefined = hoveredBar; + const bars = React.useMemo(() => { + const bars: TimelineBar[] = []; + for (const page of context.pages) { + for (const entry of page.actions) { + bars.push({ + entry, + leftTime: entry.action.startTime, + rightTime: entry.action.endTime, + leftPosition: timeToPosition(measure.width, boundaries, entry.action.startTime), + rightPosition: timeToPosition(measure.width, boundaries, entry.action.endTime), + label: entry.action.action + ' ' + (entry.action.selector || entry.action.value || ''), + type: entry.action.action, + priority: 0, + }); + if (entry === (highlightedAction || selectedAction)) + targetBar = bars[bars.length - 1]; + } + let lastDialogOpened: trace.DialogOpenedEvent | undefined; + for (const event of page.interestingEvents) { + if (event.type === 'dialog-opened') { + lastDialogOpened = event; + continue; + } + if (event.type === 'dialog-closed' && lastDialogOpened) { + bars.push({ + event, + leftTime: lastDialogOpened.timestamp, + rightTime: event.timestamp, + leftPosition: timeToPosition(measure.width, boundaries, lastDialogOpened.timestamp), + rightPosition: timeToPosition(measure.width, boundaries, event.timestamp), + label: lastDialogOpened.message ? `${event.dialogType} "${lastDialogOpened.message}"` : event.dialogType, + type: 'dialog', + priority: -1, + }); + } else if (event.type === 'navigation') { + bars.push({ + event, + leftTime: event.timestamp, + rightTime: event.timestamp, + leftPosition: timeToPosition(measure.width, boundaries, event.timestamp), + rightPosition: timeToPosition(measure.width, boundaries, event.timestamp), + label: `navigated to ${event.url}`, + type: event.type, + priority: 1, + }); + } else if (event.type === 'load') { + bars.push({ + event, + leftTime: event.timestamp, + rightTime: event.timestamp, + leftPosition: timeToPosition(measure.width, boundaries, event.timestamp), + rightPosition: timeToPosition(measure.width, boundaries, event.timestamp), + label: `load`, + type: event.type, + priority: 1, + }); + } + } + } + bars.sort((a, b) => a.priority - b.priority); + return bars; + }, [context, boundaries, measure.width]); + + const findHoveredBar = (x: number) => { const time = positionToTime(measure.width, boundaries, x); const time1 = positionToTime(measure.width, boundaries, x - 5); const time2 = positionToTime(measure.width, boundaries, x + 5); - let entry: ActionEntry | undefined; + let bar: TimelineBar | undefined; let distance: number | undefined; - for (const e of actionEntries) { - const left = Math.max(e.action.startTime!, time1); - const right = Math.min(e.action.endTime!, time2); - const middle = (e.action.startTime! + e.action.endTime!) / 2; + for (const b of bars) { + const left = Math.max(b.leftTime, time1); + const right = Math.min(b.rightTime, time2); + const middle = (b.leftTime + b.rightTime) / 2; const d = Math.abs(time - middle); - if (left <= right && (!entry || d < distance!)) { - entry = e; + if (left <= right && (!bar || d < distance!)) { + bar = b; distance = d; } } - return entry; + return bar; }; const onMouseMove = (event: React.MouseEvent) => { if (ref.current) { const x = event.clientX - ref.current.getBoundingClientRect().left; setPreviewX(x); - onHighlighted(findHoveredAction(x)); + const bar = findHoveredBar(x); + setHoveredBar(bar); + onHighlighted(bar && bar.entry ? bar.entry : undefined); } }; const onMouseLeave = () => { @@ -86,53 +148,53 @@ export const Timeline: React.FunctionComponent<{ const onClick = (event: React.MouseEvent) => { if (ref.current) { const x = event.clientX - ref.current.getBoundingClientRect().left; - const entry = findHoveredAction(x); - if (entry) - onSelected(entry); + const bar = findHoveredBar(x); + if (bar && bar.entry) + onSelected(bar.entry); } }; return
{ offsets.map((offset, index) => { - return
-
{msToString(offset.time - boundaries.minimum)}
+ return
+
{msToString(offset.time - boundaries.minimum)}
; }) }
-
{ - actionTimes.map(({ entry, left, right }) => { - return
{ + bars.map((bar, index) => { + return
- {entry.action.action} + {bar.label}
; }) }
-
{ - actionTimes.map(({ entry, left, right }) => { - return
{ + bars.map((bar, index) => { + return
; }) }
-
; }; -function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { percent: number, time: number }[] { +function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { position: number, time: number }[] { const minimumGap = 64; let dividerCount = clientWidth / minimumGap; const boundarySpan = boundaries.maximum - boundaries.minimum; @@ -157,19 +219,17 @@ function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { const offsets = []; for (let i = 0; i < dividerCount; ++i) { const time = firstDividerTime + sectionTime * i; - offsets.push({ percent: timeToPercent(clientWidth, boundaries, time), time }); + offsets.push({ position: timeToPosition(clientWidth, boundaries, time), time }); } return offsets; } -function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number): number { - const position = (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth; - return 100 * position / clientWidth; +function timeToPosition(clientWidth: number, boundaries: Boundaries, time: number): number { + return (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth; } function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number { - const percent = x / clientWidth; - return percent * (boundaries.maximum - boundaries.minimum) + boundaries.minimum; + return x / clientWidth * (boundaries.maximum - boundaries.minimum) + boundaries.minimum; } function msToString(ms: number): string { diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index fc0ede9edebfc..88b20d084f41c 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -700,6 +700,7 @@ class FrameSession { _onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) { this._page.emit(Page.Events.Dialog, new dialog.Dialog( + this._page, event.type, event.message, async (accept: boolean, promptText?: string) => { diff --git a/src/server/dialog.ts b/src/server/dialog.ts index 77c7aa01e6f5d..89067b0241823 100644 --- a/src/server/dialog.ts +++ b/src/server/dialog.ts @@ -16,25 +16,26 @@ */ import { assert } from '../utils/utils'; -import { debugLogger } from '../utils/debugLogger'; +import { Page } from './page'; type OnHandle = (accept: boolean, promptText?: string) => Promise; export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt'; export class Dialog { + private _page: Page; private _type: string; private _message: string; private _onHandle: OnHandle; private _handled = false; private _defaultValue: string; - constructor(type: string, message: string, onHandle: OnHandle, defaultValue?: string) { + constructor(page: Page, type: string, message: string, onHandle: OnHandle, defaultValue?: string) { + this._page = page; this._type = type; this._message = message; this._onHandle = onHandle; this._defaultValue = defaultValue || ''; - debugLogger.log('api', ` ${this._preview()} was shown`); } type(): string { @@ -52,22 +53,14 @@ export class Dialog { async accept(promptText: string | undefined) { assert(!this._handled, 'Cannot accept dialog which is already handled!'); this._handled = true; - debugLogger.log('api', ` ${this._preview()} was accepted`); await this._onHandle(true, promptText); + this._page.emit(Page.Events.InternalDialogClosed, this); } async dismiss() { assert(!this._handled, 'Cannot dismiss dialog which is already handled!'); this._handled = true; - debugLogger.log('api', ` ${this._preview()} was dismissed`); await this._onHandle(false); - } - - private _preview(): string { - if (!this._message) - return this._type; - if (this._message.length <= 50) - return `${this._type} "${this._message}"`; - return `${this._type} "${this._message.substring(0, 49) + '\u2026'}"`; + this._page.emit(Page.Events.InternalDialogClosed, this); } } diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 6b852dea8c0ee..9abda5242141a 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -219,6 +219,7 @@ export class FFPage implements PageDelegate { _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { this._page.emit(Page.Events.Dialog, new dialog.Dialog( + this._page, params.type, params.message, async (accept: boolean, promptText?: string) => { diff --git a/src/server/page.ts b/src/server/page.ts index 29a01cd3a36a7..3b2434d045a10 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -98,6 +98,7 @@ export class Page extends EventEmitter { Crash: 'crash', Console: 'console', Dialog: 'dialog', + InternalDialogClosed: 'internaldialogclosed', Download: 'download', FileChooser: 'filechooser', DOMContentLoaded: 'domcontentloaded', diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index a119c84d5fbe4..85699fc4f9252 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -549,6 +549,7 @@ export class WKPage implements PageDelegate { _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { this._page.emit(Page.Events.Dialog, new dialog.Dialog( + this._page, event.type as dialog.DialogType, event.message, async (accept: boolean, promptText?: string) => { diff --git a/src/trace/traceTypes.ts b/src/trace/traceTypes.ts index 54b31d68a4639..53bed033dfe4e 100644 --- a/src/trace/traceTypes.ts +++ b/src/trace/traceTypes.ts @@ -15,6 +15,7 @@ */ export type ContextCreatedTraceEvent = { + timestamp: number, type: 'context-created', browserName: string, contextId: string, @@ -24,11 +25,13 @@ export type ContextCreatedTraceEvent = { }; export type ContextDestroyedTraceEvent = { + timestamp: number, type: 'context-destroyed', contextId: string, }; export type NetworkResourceTraceEvent = { + timestamp: number, type: 'resource', contextId: string, pageId: string, @@ -40,18 +43,21 @@ export type NetworkResourceTraceEvent = { }; export type PageCreatedTraceEvent = { + timestamp: number, type: 'page-created', contextId: string, pageId: string, }; export type PageDestroyedTraceEvent = { + timestamp: number, type: 'page-destroyed', contextId: string, pageId: string, }; export type PageVideoTraceEvent = { + timestamp: number, type: 'page-video', contextId: string, pageId: string, @@ -59,6 +65,7 @@ export type PageVideoTraceEvent = { }; export type ActionTraceEvent = { + timestamp: number, type: 'action', contextId: string, action: string, @@ -66,8 +73,8 @@ export type ActionTraceEvent = { selector?: string, label?: string, value?: string, - startTime?: number, - endTime?: number, + startTime: number, + endTime: number, logs?: string[], snapshot?: { sha1: string, @@ -77,6 +84,39 @@ export type ActionTraceEvent = { error?: string, }; +export type DialogOpenedEvent = { + timestamp: number, + type: 'dialog-opened', + contextId: string, + pageId: string, + dialogType: string, + message?: string, +}; + +export type DialogClosedEvent = { + timestamp: number, + type: 'dialog-closed', + contextId: string, + pageId: string, + dialogType: string, +}; + +export type NavigationEvent = { + timestamp: number, + type: 'navigation', + contextId: string, + pageId: string, + url: string, + sameDocument: boolean, +}; + +export type LoadEvent = { + timestamp: number, + type: 'load', + contextId: string, + pageId: string, +}; + export type TraceEvent = ContextCreatedTraceEvent | ContextDestroyedTraceEvent | @@ -84,7 +124,11 @@ export type TraceEvent = PageDestroyedTraceEvent | PageVideoTraceEvent | NetworkResourceTraceEvent | - ActionTraceEvent; + ActionTraceEvent | + DialogOpenedEvent | + DialogClosedEvent | + NavigationEvent | + LoadEvent; export type FrameSnapshot = { diff --git a/src/trace/tracer.ts b/src/trace/tracer.ts index 54d818aeae579..81bcbe25333b5 100644 --- a/src/trace/tracer.ts +++ b/src/trace/tracer.ts @@ -16,7 +16,7 @@ import { ActionListener, ActionMetadata, BrowserContext, ContextListener, contextListeners, Video } from '../server/browserContext'; import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter'; -import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent, PageVideoTraceEvent } from './traceTypes'; +import * as trace from './traceTypes'; import * as path from 'path'; import * as util from 'util'; import * as fs from 'fs'; @@ -27,6 +27,8 @@ import { ElementHandle } from '../server/dom'; import { helper, RegisteredListener } from '../server/helper'; import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings'; import { ProgressResult } from '../server/progress'; +import { Dialog } from '../server/dialog'; +import { Frame, NavigationEvent } from '../server/frames'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); @@ -86,7 +88,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir); this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile); this._writeArtifactChain = Promise.resolve(); - const event: ContextCreatedTraceEvent = { + const event: trace.ContextCreatedTraceEvent = { + timestamp: monotonicTime(), type: 'context-created', browserName: context._browser._options.name, contextId: this._contextId, @@ -107,7 +110,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { } onResource(resource: SnapshotterResource): void { - const event: NetworkResourceTraceEvent = { + const event: trace.NetworkResourceTraceEvent = { + timestamp: monotonicTime(), type: 'resource', contextId: this._contextId, pageId: resource.pageId, @@ -127,7 +131,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { async onAfterAction(result: ProgressResult, metadata: ActionMetadata): Promise { try { const snapshot = await this._takeSnapshot(metadata.page, typeof metadata.target === 'string' ? undefined : metadata.target); - const event: ActionTraceEvent = { + const event: trace.ActionTraceEvent = { + timestamp: monotonicTime(), type: 'action', contextId: this._contextId, pageId: this._pageToId.get(metadata.page), @@ -150,7 +155,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { const pageId = 'page@' + createGuid(); this._pageToId.set(page, pageId); - const event: PageCreatedTraceEvent = { + const event: trace.PageCreatedTraceEvent = { + timestamp: monotonicTime(), type: 'page-created', contextId: this._contextId, pageId, @@ -160,7 +166,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { page.on(Page.Events.VideoStarted, (video: Video) => { if (this._disposed) return; - const event: PageVideoTraceEvent = { + const event: trace.PageVideoTraceEvent = { + timestamp: monotonicTime(), type: 'page-video', contextId: this._contextId, pageId, @@ -169,11 +176,65 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { this._appendTraceEvent(event); }); + page.on(Page.Events.Dialog, (dialog: Dialog) => { + if (this._disposed) + return; + const event: trace.DialogOpenedEvent = { + timestamp: monotonicTime(), + type: 'dialog-opened', + contextId: this._contextId, + pageId, + dialogType: dialog.type(), + message: dialog.message(), + }; + this._appendTraceEvent(event); + }); + + page.on(Page.Events.InternalDialogClosed, (dialog: Dialog) => { + if (this._disposed) + return; + const event: trace.DialogClosedEvent = { + timestamp: monotonicTime(), + type: 'dialog-closed', + contextId: this._contextId, + pageId, + dialogType: dialog.type(), + }; + this._appendTraceEvent(event); + }); + + page.mainFrame().on(Frame.Events.Navigation, (navigationEvent: NavigationEvent) => { + if (this._disposed || page.mainFrame().url() === 'about:blank') + return; + const event: trace.NavigationEvent = { + timestamp: monotonicTime(), + type: 'navigation', + contextId: this._contextId, + pageId, + url: navigationEvent.url, + sameDocument: !navigationEvent.newDocument, + }; + this._appendTraceEvent(event); + }); + + page.on(Page.Events.Load, () => { + if (this._disposed || page.mainFrame().url() === 'about:blank') + return; + const event: trace.LoadEvent = { + timestamp: monotonicTime(), + type: 'load', + contextId: this._contextId, + pageId, + }; + this._appendTraceEvent(event); + }); + page.once(Page.Events.Close, () => { this._pageToId.delete(page); if (this._disposed) return; - const event: PageDestroyedTraceEvent = { + const event: trace.PageDestroyedTraceEvent = { + timestamp: monotonicTime(), type: 'page-destroyed', contextId: this._contextId, pageId, @@ -204,7 +265,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { helper.removeEventListeners(this._eventListeners); this._pageToId.clear(); this._snapshotter.dispose(); - const event: ContextDestroyedTraceEvent = { + const event: trace.ContextDestroyedTraceEvent = { + timestamp: monotonicTime(), type: 'context-destroyed', contextId: this._contextId, }; @@ -234,9 +296,8 @@ class ContextTracer implements SnapshotterDelegate, ActionListener { private _appendTraceEvent(event: any) { // Serialize all writes to the trace file. - const timestamp = monotonicTime(); this._appendEventChain = this._appendEventChain.then(async traceFile => { - await fsAppendFileAsync(traceFile, JSON.stringify({...event, timestamp}) + '\n'); + await fsAppendFileAsync(traceFile, JSON.stringify(event) + '\n'); return traceFile; }); }