fix(trace-viewer): include waitFor* in trace viewer (#7413)

This commit is contained in:
Pavel Feldman 2021-06-30 17:56:48 -07:00 committed by GitHub
parent 63e6e530ca
commit f43b4efbc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 200 additions and 114 deletions

View File

@ -26,7 +26,6 @@ import { Page } from './page';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { ParsedStackTrace } from '../utils/stackTrace';
type Direction = 'down' | 'up' | 'left' | 'right'; type Direction = 'down' | 'up' | 'left' | 'right';
type SpeedOptions = { speed?: number }; type SpeedOptions = { speed?: number };
@ -236,10 +235,10 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
} }
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event, stackTrace); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
if (event !== Events.AndroidDevice.Close) if (event !== Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed')); waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed'));

View File

@ -33,7 +33,6 @@ import * as api from '../../types/types';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
import { CDPSession } from './cdpSession'; import { CDPSession } from './cdpSession';
import { Tracing } from './tracing'; import { Tracing } from './tracing';
import { ParsedStackTrace } from '../utils/stackTrace';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext { export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
_pages = new Set<Page>(); _pages = new Set<Page>();
@ -270,10 +269,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
} }
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async (channel: channels.BrowserContextChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event, stackTrace); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
if (event !== Events.BrowserContext.Close) if (event !== Events.BrowserContext.Close)
waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed')); waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed'));

View File

@ -107,18 +107,6 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
} }
} }
_waitForEventInfoBefore(waitId: string, event: string, stackTrace: ParsedStackTrace) {
this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { waitId, phase: 'before', event } }, stackTrace).catch(() => {});
}
_waitForEventInfoAfter(waitId: string, error: string | undefined) {
this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { waitId, phase: 'after', error } }, null).catch(() => {});
}
_waitForEventInfoLog(waitId: string, message: string) {
this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { waitId, phase: 'log', message } }, null).catch(() => {});
}
private toJSON() { private toJSON() {
// Jest's expect library tries to print objects sometimes. // Jest's expect library tries to print objects sometimes.
// RPC objects can contain links to lots of other objects, // RPC objects can contain links to lots of other objects,

View File

@ -18,7 +18,6 @@ import type { BrowserWindow } from 'electron';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
import * as api from '../../types/types'; import * as api from '../../types/types';
import * as channels from '../protocol/channels'; import * as channels from '../protocol/channels';
import { ParsedStackTrace } from '../utils/stackTrace';
import { TimeoutSettings } from '../utils/timeoutSettings'; import { TimeoutSettings } from '../utils/timeoutSettings';
import { headersObjectToArray } from '../utils/utils'; import { headersObjectToArray } from '../utils/utils';
import { BrowserContext } from './browserContext'; import { BrowserContext } from './browserContext';
@ -101,16 +100,16 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
} }
async close() { async close() {
return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => {
await channel.close(); await channel.close();
}); });
} }
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event, stackTrace); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
if (event !== Events.ElectronApplication.Close) if (event !== Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed')); waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed'));

View File

@ -30,7 +30,6 @@ import { LifecycleEvent, URLMatch, SelectOption, SelectOptionOptions, FilePayloa
import { urlMatches } from './clientHelper'; import { urlMatches } from './clientHelper';
import * as api from '../../types/types'; import * as api from '../../types/types';
import * as structs from '../../types/structs'; import * as structs from '../../types/structs';
import { ParsedStackTrace } from '../utils/stackTrace';
export type WaitForNavigationOptions = { export type WaitForNavigationOptions = {
timeout?: number, timeout?: number,
@ -94,8 +93,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
}); });
} }
private _setupNavigationWaiter(options: { timeout?: number }, stackTrace: ParsedStackTrace): Waiter { private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
const waiter = new Waiter(this, '', stackTrace); const waiter = new Waiter(this._page!, '');
if (this._page!.isClosed()) if (this._page!.isClosed())
waiter.rejectImmediately(new Error('Navigation failed because page was closed!')); waiter.rejectImmediately(new Error('Navigation failed because page was closed!'));
waiter.rejectOnEvent(this._page!, Events.Page.Close, new Error('Navigation failed because page was closed!')); waiter.rejectOnEvent(this._page!, Events.Page.Close, new Error('Navigation failed because page was closed!'));
@ -107,9 +106,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
} }
async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> { async waitForNavigation(options: WaitForNavigationOptions = {}): Promise<network.Response | null> {
return this._wrapApiCall(async (channel: channels.FrameChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.FrameChannel) => {
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
const waiter = this._setupNavigationWaiter(options, stackTrace); const waiter = this._setupNavigationWaiter(options);
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`); waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
@ -145,8 +144,8 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
state = verifyLoadState('state', state); state = verifyLoadState('state', state);
if (this._loadStates.has(state)) if (this._loadStates.has(state))
return; return;
return this._wrapApiCall(async (channel: channels.FrameChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.FrameChannel) => {
const waiter = this._setupNavigationWaiter(options, stackTrace); const waiter = this._setupNavigationWaiter(options);
await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => { await waiter.waitForEvent<LifecycleEvent>(this._eventEmitter, 'loadstate', s => {
waiter.log(` "${s}" event fired`); waiter.log(` "${s}" event fired`);
return s === state; return s === state;

View File

@ -26,7 +26,6 @@ import { Events } from './events';
import { Page } from './page'; import { Page } from './page';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import * as api from '../../types/types'; import * as api from '../../types/types';
import { ParsedStackTrace } from '../utils/stackTrace';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -476,10 +475,10 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.
} }
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async (channel: channels.WebSocketChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.WebSocketChannel) => {
const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event, stackTrace); const waiter = Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
if (event !== Events.WebSocket.Error) if (event !== Events.WebSocket.Error)
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error')); waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));

View File

@ -46,7 +46,6 @@ import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } fro
import { isSafeCloseError } from '../utils/errors'; import { isSafeCloseError } from '../utils/errors';
import { Video } from './video'; import { Video } from './video';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import { ParsedStackTrace } from '../utils/stackTrace';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & { type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
width?: string | number, width?: string | number,
@ -349,7 +348,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
} }
async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Request> { async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Request> {
return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.PageChannel) => {
const predicate = (request: Request) => { const predicate = (request: Request) => {
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) if (isString(urlOrPredicate) || isRegExp(urlOrPredicate))
return urlMatches(request.url(), urlOrPredicate); return urlMatches(request.url(), urlOrPredicate);
@ -357,12 +356,12 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
}; };
const trimmedUrl = trimUrl(urlOrPredicate); const trimmedUrl = trimUrl(urlOrPredicate);
const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined; const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined;
return this._waitForEvent(Events.Page.Request, { predicate, timeout: options.timeout }, stackTrace, logLine); return this._waitForEvent(Events.Page.Request, { predicate, timeout: options.timeout }, logLine);
}); });
} }
async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Response> { async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Response> {
return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.PageChannel) => {
const predicate = (response: Response) => { const predicate = (response: Response) => {
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) if (isString(urlOrPredicate) || isRegExp(urlOrPredicate))
return urlMatches(response.url(), urlOrPredicate); return urlMatches(response.url(), urlOrPredicate);
@ -370,20 +369,20 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
}; };
const trimmedUrl = trimUrl(urlOrPredicate); const trimmedUrl = trimUrl(urlOrPredicate);
const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined; const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined;
return this._waitForEvent(Events.Page.Response, { predicate, timeout: options.timeout }, stackTrace, logLine); return this._waitForEvent(Events.Page.Response, { predicate, timeout: options.timeout }, logLine);
}); });
} }
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> { async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { return this._wrapApiCall(async (channel: channels.PageChannel) => {
return this._waitForEvent(event, optionsOrPredicate, stackTrace, `waiting for event "${event}"`); return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
}); });
} }
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, stackTrace: ParsedStackTrace, logLine?: string): Promise<any> { private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this, event, stackTrace); const waiter = Waiter.createForEvent(this, event);
if (logLine) if (logLine)
waiter.log(logLine); waiter.log(logLine);
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);

View File

@ -15,9 +15,10 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { ParsedStackTrace, rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
import { TimeoutError } from '../utils/errors'; import { TimeoutError } from '../utils/errors';
import { createGuid } from '../utils/utils'; import { createGuid } from '../utils/utils';
import * as channels from '../protocol/channels';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
export class Waiter { export class Waiter {
@ -26,21 +27,23 @@ export class Waiter {
private _immediateError?: Error; private _immediateError?: Error;
// TODO: can/should we move these logs into wrapApiCall? // TODO: can/should we move these logs into wrapApiCall?
private _logs: string[] = []; private _logs: string[] = [];
private _channelOwner: ChannelOwner; private _channel: channels.EventTargetChannel;
private _waitId: string; private _waitId: string;
private _error: string | undefined; private _error: string | undefined;
constructor(channelOwner: ChannelOwner, event: string, stackTrace: ParsedStackTrace) { constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
this._waitId = createGuid(); this._waitId = createGuid();
this._channelOwner = channelOwner; this._channel = channelOwner._channel;
this._channelOwner._waitForEventInfoBefore(this._waitId, event, stackTrace); channelOwner._wrapApiCall(async (channel: channels.EventTargetChannel) => {
channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
});
this._dispose = [ this._dispose = [
() => this._channelOwner._waitForEventInfoAfter(this._waitId, this._error) () => this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'after', error: this._error } }).catch(() => {})
]; ];
} }
static createForEvent(channelOwner: ChannelOwner, event: string, stackTrace: ParsedStackTrace) { static createForEvent(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
return new Waiter(channelOwner, event, stackTrace); return new Waiter(channelOwner, event);
} }
async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): Promise<T> { async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): Promise<T> {
@ -89,7 +92,7 @@ export class Waiter {
log(s: string) { log(s: string) {
this._logs.push(s); this._logs.push(s);
this._channelOwner._waitForEventInfoLog(this._waitId, s); this._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } }).catch(() => {});
} }
private _rejectOn(promise: Promise<any>, dispose?: () => void) { private _rejectOn(promise: Promise<any>, dispose?: () => void) {

View File

@ -77,7 +77,7 @@ export class Dispatcher<Type extends { guid: string }, Initializer> extends Even
(object as any)[dispatcherSymbol] = this; (object as any)[dispatcherSymbol] = this;
if (this._parent) if (this._parent)
this._connection.sendMessageToClient(this._parent._guid, type, '__create__', { type, initializer, guid }); this._connection.sendMessageToClient(this._parent._guid, type, '__create__', { type, initializer, guid }, this._parent._object);
} }
_dispatchEvent(method: string, params: Dispatcher<any, any> | any = {}) { _dispatchEvent(method: string, params: Dispatcher<any, any> | any = {}) {
@ -142,8 +142,8 @@ export class DispatcherConnection {
const eventMetadata: CallMetadata = { const eventMetadata: CallMetadata = {
id: `event@${++lastEventId}`, id: `event@${++lastEventId}`,
objectId: sdkObject?.guid, objectId: sdkObject?.guid,
pageId: sdkObject?.attribution.page?.guid, pageId: sdkObject?.attribution?.page?.guid,
frameId: sdkObject?.attribution.frame?.guid, frameId: sdkObject?.attribution?.frame?.guid,
startTime: monotonicTime(), startTime: monotonicTime(),
endTime: 0, endTime: 0,
type, type,
@ -152,7 +152,7 @@ export class DispatcherConnection {
log: [], log: [],
snapshots: [] snapshots: []
}; };
sdkObject.instrumentation.onEvent(sdkObject, eventMetadata); sdkObject.instrumentation?.onEvent(sdkObject, eventMetadata);
} }
this.onmessage({ guid, method, params }); this.onmessage({ guid, method, params });
} }
@ -176,8 +176,6 @@ export class DispatcherConnection {
}; };
const scheme = createScheme(tChannel); const scheme = createScheme(tChannel);
this._validateParams = (type: string, method: string, params: any): any => { this._validateParams = (type: string, method: string, params: any): any => {
if (method === 'waitForEventInfo')
return tOptional(scheme['WaitForEventInfo'])(params.info, '');
const name = type + method[0].toUpperCase() + method.substring(1) + 'Params'; const name = type + method[0].toUpperCase() + method.substring(1) + 'Params';
if (!scheme[name]) if (!scheme[name])
throw new ValidationError(`Unknown scheme for ${type}.${method}`); throw new ValidationError(`Unknown scheme for ${type}.${method}`);
@ -221,8 +219,8 @@ export class DispatcherConnection {
id: `call@${id}`, id: `call@${id}`,
...validMetadata, ...validMetadata,
objectId: sdkObject?.guid, objectId: sdkObject?.guid,
pageId: sdkObject?.attribution.page?.guid, pageId: sdkObject?.attribution?.page?.guid,
frameId: sdkObject?.attribution.frame?.guid, frameId: sdkObject?.attribution?.frame?.guid,
startTime: monotonicTime(), startTime: monotonicTime(),
endTime: 0, endTime: 0,
type: dispatcher._type, type: dispatcher._type,

View File

@ -35,14 +35,6 @@ export type Metadata = {
apiName?: string, apiName?: string,
}; };
export type WaitForEventInfo = {
waitId: string,
phase: 'before' | 'after' | 'log',
event?: string,
message?: string,
error?: string,
};
export type Point = { export type Point = {
x: number, x: number,
y: number, y: number,
@ -604,11 +596,30 @@ export type BrowserStopTracingResult = {
binary: Binary, binary: Binary,
}; };
// ----------- EventTarget -----------
export type EventTargetInitializer = {};
export interface EventTargetChannel extends Channel {
waitForEventInfo(params: EventTargetWaitForEventInfoParams, metadata?: Metadata): Promise<EventTargetWaitForEventInfoResult>;
}
export type EventTargetWaitForEventInfoParams = {
info: {
waitId: string,
phase: 'before' | 'after' | 'log',
event?: string,
message?: string,
error?: string,
},
};
export type EventTargetWaitForEventInfoOptions = {
};
export type EventTargetWaitForEventInfoResult = void;
// ----------- BrowserContext ----------- // ----------- BrowserContext -----------
export type BrowserContextInitializer = { export type BrowserContextInitializer = {
isChromium: boolean, isChromium: boolean,
}; };
export interface BrowserContextChannel extends Channel { export interface BrowserContextChannel extends EventTargetChannel {
on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this; on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this;
on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this; on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this;
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
@ -868,7 +879,7 @@ export type PageInitializer = {
isClosed: boolean, isClosed: boolean,
opener?: PageChannel, opener?: PageChannel,
}; };
export interface PageChannel extends Channel { export interface PageChannel extends EventTargetChannel {
on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => void): this; on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => void): this;
on(event: 'close', callback: (params: PageCloseEvent) => void): this; on(event: 'close', callback: (params: PageCloseEvent) => void): this;
on(event: 'console', callback: (params: PageConsoleEvent) => void): this; on(event: 'console', callback: (params: PageConsoleEvent) => void): this;
@ -2460,7 +2471,7 @@ export type RemoteAddr = {
export type WebSocketInitializer = { export type WebSocketInitializer = {
url: string, url: string,
}; };
export interface WebSocketChannel extends Channel { export interface WebSocketChannel extends EventTargetChannel {
on(event: 'open', callback: (params: WebSocketOpenEvent) => void): this; on(event: 'open', callback: (params: WebSocketOpenEvent) => void): this;
on(event: 'frameSent', callback: (params: WebSocketFrameSentEvent) => void): this; on(event: 'frameSent', callback: (params: WebSocketFrameSentEvent) => void): this;
on(event: 'frameReceived', callback: (params: WebSocketFrameReceivedEvent) => void): this; on(event: 'frameReceived', callback: (params: WebSocketFrameReceivedEvent) => void): this;
@ -2717,7 +2728,7 @@ export type ElectronLaunchResult = {
export type ElectronApplicationInitializer = { export type ElectronApplicationInitializer = {
context: BrowserContextChannel, context: BrowserContextChannel,
}; };
export interface ElectronApplicationChannel extends Channel { export interface ElectronApplicationChannel extends EventTargetChannel {
on(event: 'close', callback: (params: ElectronApplicationCloseEvent) => void): this; on(event: 'close', callback: (params: ElectronApplicationCloseEvent) => void): this;
browserWindow(params: ElectronApplicationBrowserWindowParams, metadata?: Metadata): Promise<ElectronApplicationBrowserWindowResult>; browserWindow(params: ElectronApplicationBrowserWindowParams, metadata?: Metadata): Promise<ElectronApplicationBrowserWindowResult>;
evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, metadata?: Metadata): Promise<ElectronApplicationEvaluateExpressionResult>; evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, metadata?: Metadata): Promise<ElectronApplicationEvaluateExpressionResult>;
@ -2807,7 +2818,7 @@ export type AndroidDeviceInitializer = {
model: string, model: string,
serial: string, serial: string,
}; };
export interface AndroidDeviceChannel extends Channel { export interface AndroidDeviceChannel extends EventTargetChannel {
on(event: 'webViewAdded', callback: (params: AndroidDeviceWebViewAddedEvent) => void): this; on(event: 'webViewAdded', callback: (params: AndroidDeviceWebViewAddedEvent) => void): this;
on(event: 'webViewRemoved', callback: (params: AndroidDeviceWebViewRemovedEvent) => void): this; on(event: 'webViewRemoved', callback: (params: AndroidDeviceWebViewRemovedEvent) => void): this;
wait(params: AndroidDeviceWaitParams, metadata?: Metadata): Promise<AndroidDeviceWaitResult>; wait(params: AndroidDeviceWaitParams, metadata?: Metadata): Promise<AndroidDeviceWaitResult>;
@ -3237,6 +3248,12 @@ export type SocksSocketEndOptions = {};
export type SocksSocketEndResult = void; export type SocksSocketEndResult = void;
export const commandsWithTracingSnapshots = new Set([ export const commandsWithTracingSnapshots = new Set([
'EventTarget.waitForEventInfo',
'BrowserContext.waitForEventInfo',
'Page.waitForEventInfo',
'WebSocket.waitForEventInfo',
'ElectronApplication.waitForEventInfo',
'AndroidDevice.waitForEventInfo',
'Page.goBack', 'Page.goBack',
'Page.goForward', 'Page.goForward',
'Page.reload', 'Page.reload',
@ -3285,7 +3302,9 @@ export const commandsWithTracingSnapshots = new Set([
'Frame.waitForFunction', 'Frame.waitForFunction',
'Frame.waitForSelector', 'Frame.waitForSelector',
'JSHandle.evaluateExpression', 'JSHandle.evaluateExpression',
'ElementHandle.evaluateExpression',
'JSHandle.evaluateExpressionHandle', 'JSHandle.evaluateExpressionHandle',
'ElementHandle.evaluateExpressionHandle',
'ElementHandle.evalOnSelector', 'ElementHandle.evalOnSelector',
'ElementHandle.evalOnSelectorAll', 'ElementHandle.evalOnSelectorAll',
'ElementHandle.check', 'ElementHandle.check',

View File

@ -31,21 +31,6 @@ Metadata:
apiName: string? apiName: string?
WaitForEventInfo:
type: object
properties:
waitId: string
phase:
type: enum
literals:
- before
- after
- log
event: string?
message: string?
error: string?
Point: Point:
type: object type: object
properties: properties:
@ -504,10 +489,33 @@ Browser:
close: close:
EventTarget:
type: interface
commands:
waitForEventInfo:
parameters:
info:
type: object
properties:
waitId: string
phase:
type: enum
literals:
- before
- after
- log
event: string?
message: string?
error: string?
tracing:
snapshot: true
BrowserContext: BrowserContext:
type: interface type: interface
extends: EventTarget
initializer: initializer:
isChromium: boolean isChromium: boolean
@ -691,6 +699,8 @@ BrowserContext:
Page: Page:
type: interface type: interface
extends: EventTarget
initializer: initializer:
mainFrame: Frame mainFrame: Frame
viewportSize: viewportSize:
@ -2128,6 +2138,8 @@ RemoteAddr:
WebSocket: WebSocket:
type: interface type: interface
extends: EventTarget
initializer: initializer:
url: string url: string
@ -2346,6 +2358,8 @@ Electron:
ElectronApplication: ElectronApplication:
type: interface type: interface
extends: EventTarget
initializer: initializer:
context: BrowserContext context: BrowserContext
@ -2413,6 +2427,8 @@ AndroidSocket:
AndroidDevice: AndroidDevice:
type: interface type: interface
extends: EventTarget
initializer: initializer:
model: string model: string
serial: string serial: string

View File

@ -43,13 +43,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
stack: tOptional(tArray(tType('StackFrame'))), stack: tOptional(tArray(tType('StackFrame'))),
apiName: tOptional(tString), apiName: tOptional(tString),
}); });
scheme.WaitForEventInfo = tObject({
waitId: tString,
phase: tEnum(['before', 'after', 'log']),
event: tOptional(tString),
message: tOptional(tString),
error: tOptional(tString),
});
scheme.Point = tObject({ scheme.Point = tObject({
x: tNumber, x: tNumber,
y: tNumber, y: tNumber,
@ -335,6 +328,20 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
categories: tOptional(tArray(tString)), categories: tOptional(tArray(tString)),
}); });
scheme.BrowserStopTracingParams = tOptional(tObject({})); scheme.BrowserStopTracingParams = tOptional(tObject({}));
scheme.EventTargetWaitForEventInfoParams = tObject({
info: tObject({
waitId: tString,
phase: tEnum(['before', 'after', 'log']),
event: tOptional(tString),
message: tOptional(tString),
error: tOptional(tString),
}),
});
scheme.BrowserContextWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.PageWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.WebSocketWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.ElectronApplicationWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.AndroidDeviceWaitForEventInfoParams = tType('EventTargetWaitForEventInfoParams');
scheme.BrowserContextAddCookiesParams = tObject({ scheme.BrowserContextAddCookiesParams = tObject({
cookies: tArray(tType('SetNetworkCookie')), cookies: tArray(tType('SetNetworkCookie')),
}); });

View File

@ -133,7 +133,7 @@ export class Tracing implements InstrumentationListener {
return; return;
if (!this._snapshotter.started()) if (!this._snapshotter.started())
return; return;
if (!this._shouldCaptureSnapshot(metadata)) if (!shouldCaptureSnapshot(metadata))
return; return;
const snapshotName = `${name}@${metadata.id}`; const snapshotName = `${name}@${metadata.id}`;
metadata.snapshots.push({ title: name, snapshotName }); metadata.snapshots.push({ title: name, snapshotName });
@ -162,7 +162,7 @@ export class Tracing implements InstrumentationListener {
} }
pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata); pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata);
await pendingCall.afterSnapshot; await pendingCall.afterSnapshot;
const event: trace.ActionTraceEvent = { type: 'action', metadata, hasSnapshot: this._shouldCaptureSnapshot(metadata) }; const event: trace.ActionTraceEvent = { type: 'action', metadata, hasSnapshot: shouldCaptureSnapshot(metadata) };
this._appendTraceEvent(event); this._appendTraceEvent(event);
this._pendingCalls.delete(metadata.id); this._pendingCalls.delete(metadata.id);
} }
@ -225,8 +225,8 @@ export class Tracing implements InstrumentationListener {
await fs.promises.appendFile(this._traceFile!, JSON.stringify(event) + '\n'); await fs.promises.appendFile(this._traceFile!, JSON.stringify(event) + '\n');
}); });
} }
}
private _shouldCaptureSnapshot(metadata: CallMetadata): boolean { function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method); return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
} }
}

View File

@ -43,9 +43,8 @@ export class TraceModel {
appendEvents(events: trace.TraceEvent[], snapshotStorage: SnapshotStorage) { appendEvents(events: trace.TraceEvent[], snapshotStorage: SnapshotStorage) {
for (const event of events) for (const event of events)
this.appendEvent(event); this.appendEvent(event);
const actions: trace.ActionTraceEvent[] = [];
for (const page of this.contextEntry!.pages) for (const page of this.contextEntry!.pages)
actions.push(...page.actions); page.actions.sort((a1, a2) => a1.metadata.startTime - a2.metadata.startTime);
this.contextEntry!.resources = snapshotStorage.resources(); this.contextEntry!.resources = snapshotStorage.resources();
} }
@ -55,6 +54,7 @@ export class TraceModel {
pageEntry = { pageEntry = {
actions: [], actions: [],
events: [], events: [],
objects: {},
screencastFrames: [], screencastFrames: [],
}; };
this.pageEntries.set(pageId, pageEntry); this.pageEntries.set(pageId, pageEntry);
@ -83,8 +83,12 @@ export class TraceModel {
} }
case 'event': { case 'event': {
const metadata = event.metadata; const metadata = event.metadata;
if (metadata.pageId) if (metadata.pageId) {
if (metadata.method === '__create__')
this._pageEntry(metadata.pageId).objects[metadata.params.guid] = metadata.params.initializer;
else
this._pageEntry(metadata.pageId).events.push(event); this._pageEntry(metadata.pageId).events.push(event);
}
break; break;
} }
case 'resource-snapshot': case 'resource-snapshot':
@ -113,6 +117,7 @@ export type ContextEntry = {
export type PageEntry = { export type PageEntry = {
actions: trace.ActionTraceEvent[]; actions: trace.ActionTraceEvent[];
events: trace.ActionTraceEvent[]; events: trace.ActionTraceEvent[];
objects: { [ket: string]: any };
screencastFrames: { screencastFrames: {
sha1: string, sha1: string,
timestamp: number, timestamp: number,

View File

@ -141,7 +141,10 @@ export class TraceViewer {
}); });
await context.extendInjectedScript('main', consoleApiSource.source); await context.extendInjectedScript('main', consoleApiSource.source);
const [page] = context.pages(); const [page] = context.pages();
if (isUnderTest())
page.on('close', () => context.close(internalCallMetadata()).catch(() => {})); page.on('close', () => context.close(internalCallMetadata()).catch(() => {}));
else
page.on('close', () => process.exit());
await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/traceviewer/traceViewer/index.html'); await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/traceviewer/traceViewer/index.html');
return context; return context;
} }

View File

@ -22,6 +22,7 @@
user-select: none; user-select: none;
} }
.codicon-blank:before { content: '\81'; }
.codicon-add:before { content: '\ea60'; } .codicon-add:before { content: '\ea60'; }
.codicon-plus:before { content: '\ea60'; } .codicon-plus:before { content: '\ea60'; }
.codicon-gist-new:before { content: '\ea60'; } .codicon-gist-new:before { content: '\ea60'; }

View File

@ -42,7 +42,7 @@ export const FilmStrip: React.FunctionComponent<{
if (previewPoint !== undefined && screencastFrames) { if (previewPoint !== undefined && screencastFrames) {
const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewPoint.x / measure.width; const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewPoint.x / measure.width;
previewImage = screencastFrames[upperBound(screencastFrames, previewTime, timeComparator) - 1]; previewImage = screencastFrames[upperBound(screencastFrames, previewTime, timeComparator) - 1];
previewSize = inscribe({width: previewImage.width, height: previewImage.height}, { width: 600, height: 600 }); previewSize = previewImage ? inscribe({width: previewImage.width, height: previewImage.height}, { width: 600, height: 600 }) : undefined;
} }
return <div className='film-strip' ref={ref}>{ return <div className='film-strip' ref={ref}>{

View File

@ -44,7 +44,7 @@
} }
.tab-element { .tab-element {
padding: 2px 6px 0 6px; padding: 2px 12px 0 12px;
margin-right: 4px; margin-right: 4px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -53,7 +53,6 @@
justify-content: center; justify-content: center;
user-select: none; user-select: none;
border-bottom: 3px solid transparent; border-bottom: 3px solid transparent;
width: 80px;
outline: none; outline: none;
height: 100%; height: 100%;
} }

View File

@ -170,8 +170,8 @@ export const Timeline: React.FunctionComponent<{
return <div key={index} return <div key={index}
className={'timeline-label ' + bar.className + (targetBar === bar ? ' selected' : '')} className={'timeline-label ' + bar.className + (targetBar === bar ? ' selected' : '')}
style={{ style={{
left: bar.leftPosition + 'px', left: bar.leftPosition,
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px', maxWidth: 100,
}} }}
> >
{bar.label} {bar.label}

View File

@ -54,6 +54,9 @@ export const Workbench: React.FunctionComponent<{
const snapshotSize = context.options.viewport || { width: 1280, height: 720 }; const snapshotSize = context.options.viewport || { width: 1280, height: 720 };
const boundaries = { minimum: context.startTime, maximum: context.endTime }; const boundaries = { minimum: context.startTime, maximum: context.endTime };
// Leave some nice free space on the right hand side.
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
return <div className='vbox workbench'> return <div className='vbox workbench'>
<div className='hbox header'> <div className='hbox header'>
<div className='logo'>🎭</div> <div className='logo'>🎭</div>

View File

@ -24,8 +24,8 @@ class TraceViewerPage {
constructor(public page: Page) {} constructor(public page: Page) {}
async actionTitles() { async actionTitles() {
await this.page.waitForSelector('.action-title'); await this.page.waitForSelector('.action-title:visible');
return await this.page.$$eval('.action-title', ee => ee.map(e => e.textContent)); return await this.page.$$eval('.action-title:visible', ee => ee.map(e => e.textContent));
} }
async selectAction(title: string) { async selectAction(title: string) {
@ -33,7 +33,20 @@ class TraceViewerPage {
} }
async logLines() { async logLines() {
return await this.page.$$eval('.log-line', ee => ee.map(e => e.textContent)); await this.page.waitForSelector('.log-line:visible');
return await this.page.$$eval('.log-line:visible', ee => ee.map(e => e.textContent));
}
async eventBars() {
await this.page.waitForSelector('.timeline-bar.event:visible');
const list = await this.page.$$eval('.timeline-bar.event:visible', ee => ee.map(e => e.className));
const set = new Set<string>();
for (const item of list) {
for (const className of item.split(' '))
set.add(className);
}
const result = [...set];
return result.sort();
} }
} }
@ -60,6 +73,15 @@ test.beforeAll(async ({ browser }, workerInfo) => {
await page.goto('data:text/html,<html>Hello world</html>'); await page.goto('data:text/html,<html>Hello world</html>');
await page.setContent('<button>Click</button>'); await page.setContent('<button>Click</button>');
await page.click('"Click"'); await page.click('"Click"');
await Promise.all([
page.waitForNavigation(),
page.waitForTimeout(200).then(() => page.goto('data:text/html,<html>Hello world 2</html>'))
]);
await page.evaluate(() => {
console.log('Log');
console.warn('Warning');
console.error('Error');
});
await page.close(); await page.close();
traceFile = path.join(workerInfo.project.outputDir, 'trace.zip'); traceFile = path.join(workerInfo.project.outputDir, 'trace.zip');
await context.tracing.stop({ path: traceFile }); await context.tracing.stop({ path: traceFile });
@ -72,7 +94,14 @@ test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) =>
test('should open simple trace viewer', async ({ showTraceViewer }) => { test('should open simple trace viewer', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(traceFile); const traceViewer = await showTraceViewer(traceFile);
expect(await traceViewer.actionTitles()).toEqual(['page.goto', 'page.setContent', 'page.click']); expect(await traceViewer.actionTitles()).toEqual([
'page.goto',
'page.setContent',
'page.click',
'page.waitForNavigation',
'page.goto',
'page.evaluate'
]);
}); });
test('should contain action log', async ({ showTraceViewer }) => { test('should contain action log', async ({ showTraceViewer }) => {
@ -84,3 +113,9 @@ test('should contain action log', async ({ showTraceViewer }) => {
expect(logLines).toContain('attempting click action'); expect(logLines).toContain('attempting click action');
expect(logLines).toContain(' click action done'); expect(logLines).toContain(' click action done');
}); });
test('should render events', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(traceFile);
const events = await traceViewer.eventBars();
expect(events).toContain('page_console');
});

View File

@ -187,6 +187,18 @@ for (const [name, value] of Object.entries(protocol)) {
mixins.set(name, value); mixins.set(name, value);
} }
const derivedClasses = new Map();
for (const [name, item] of Object.entries(protocol)) {
if (item.type === 'interface' && item.extends) {
let items = derivedClasses.get(item.extends);
if (!items) {
items = [];
derivedClasses.set(item.extends, items);
}
items.push(name);
}
}
for (const [name, item] of Object.entries(protocol)) { for (const [name, item] of Object.entries(protocol)) {
if (item.type === 'interface') { if (item.type === 'interface') {
const channelName = name; const channelName = name;
@ -210,8 +222,11 @@ for (const [name, item] of Object.entries(protocol)) {
for (let [methodName, method] of Object.entries(item.commands || {})) { for (let [methodName, method] of Object.entries(item.commands || {})) {
if (method === null) if (method === null)
method = {}; method = {};
if (method.tracing && method.tracing.snapshot) if (method.tracing && method.tracing.snapshot) {
tracingSnapshots.push(name + '.' + methodName); tracingSnapshots.push(name + '.' + methodName);
for (const derived of derivedClasses.get(name) || [])
tracingSnapshots.push(derived + '.' + methodName);
}
const parameters = objectType(method.parameters || {}, ''); const parameters = objectType(method.parameters || {}, '');
const paramsName = `${channelName}${titleCase(methodName)}Params`; const paramsName = `${channelName}${titleCase(methodName)}Params`;
const optionsName = `${channelName}${titleCase(methodName)}Options`; const optionsName = `${channelName}${titleCase(methodName)}Options`;