mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(trace): show dialogs, navigations and misc events (#5025)
This commit is contained in:
parent
e67d563798
commit
afaec552dd
@ -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());
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
--purple: #9C27B0;
|
||||
--yellow: #FFC107;
|
||||
--blue: #2196F3;
|
||||
--transparent-blue: #2196F355;
|
||||
--orange: #d24726;
|
||||
--black: #1E1E1E;
|
||||
--gray: #888888;
|
||||
|
||||
@ -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<TraceModel>;
|
||||
getVideoMetaInfo(videoId: string): Promise<VideoMetaInfo | undefined>;
|
||||
readFile(filePath: string): Promise<string>;
|
||||
renderSnapshot(action: ActionTraceEvent): void;
|
||||
renderSnapshot(action: trace.ActionTraceEvent): void;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<HTMLDivElement>();
|
||||
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
||||
const targetAction = highlightedAction || selectedAction;
|
||||
const [hoveredBar, setHoveredBar] = React.useState<TimelineBar | undefined>();
|
||||
|
||||
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 <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
|
||||
<div className='timeline-grid'>{
|
||||
offsets.map((offset, index) => {
|
||||
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
|
||||
<div className='timeline-label'>{msToString(offset.time - boundaries.minimum)}</div>
|
||||
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
|
||||
<div className='timeline-time'>{msToString(offset.time - boundaries.minimum)}</div>
|
||||
</div>;
|
||||
})
|
||||
}</div>
|
||||
<div className='timeline-lane timeline-action-labels'>{
|
||||
actionTimes.map(({ entry, left, right }) => {
|
||||
return <div key={entry.actionId}
|
||||
className={'timeline-action-label ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
||||
<div className='timeline-lane timeline-labels'>{
|
||||
bars.map((bar, index) => {
|
||||
return <div key={index}
|
||||
className={'timeline-label ' + bar.type + (targetBar === bar ? ' selected' : '')}
|
||||
style={{
|
||||
left: left + '%',
|
||||
width: (right - left) + '%',
|
||||
left: bar.leftPosition + 'px',
|
||||
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
|
||||
}}
|
||||
>
|
||||
{entry.action.action}
|
||||
{bar.label}
|
||||
</div>;
|
||||
})
|
||||
}</div>
|
||||
<div className='timeline-lane timeline-actions'>{
|
||||
actionTimes.map(({ entry, left, right }) => {
|
||||
return <div key={entry.actionId}
|
||||
className={'timeline-action ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
||||
<div className='timeline-lane timeline-bars'>{
|
||||
bars.map((bar, index) => {
|
||||
return <div key={index}
|
||||
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}
|
||||
style={{
|
||||
left: left + '%',
|
||||
width: (right - left) + '%',
|
||||
left: bar.leftPosition + 'px',
|
||||
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
|
||||
}}
|
||||
></div>;
|
||||
})
|
||||
}</div>
|
||||
<FilmStrip context={context} boundaries={boundaries} previewX={previewX} />
|
||||
<div className='timeline-time-bar timeline-time-bar-hover' style={{
|
||||
<div className='timeline-marker timeline-marker-hover' style={{
|
||||
display: (previewX !== undefined) ? 'block' : 'none',
|
||||
left: (previewX || 0) + 'px',
|
||||
}}></div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
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 {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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<void>;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -98,6 +98,7 @@ export class Page extends EventEmitter {
|
||||
Crash: 'crash',
|
||||
Console: 'console',
|
||||
Dialog: 'dialog',
|
||||
InternalDialogClosed: 'internaldialogclosed',
|
||||
Download: 'download',
|
||||
FileChooser: 'filechooser',
|
||||
DOMContentLoaded: 'domcontentloaded',
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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<void> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user