diff --git a/src/client/android.ts b/src/client/android.ts index ad33a3bdbf..8fc54deb41 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -26,7 +26,6 @@ import { Page } from './page'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { Waiter } from './waiter'; import { EventEmitter } from 'events'; -import { ParsedStackTrace } from '../utils/stackTrace'; type Direction = 'down' | 'up' | 'left' | 'right'; type SpeedOptions = { speed?: number }; @@ -236,10 +235,10 @@ export class AndroidDevice extends ChannelOwner { - 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 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}"`); if (event !== Events.AndroidDevice.Close) waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed')); diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index f9678d3cd8..c0ff8e42a2 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -33,7 +33,6 @@ import * as api from '../../types/types'; import * as structs from '../../types/structs'; import { CDPSession } from './cdpSession'; import { Tracing } from './tracing'; -import { ParsedStackTrace } from '../utils/stackTrace'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); @@ -270,10 +269,10 @@ export class BrowserContext extends ChannelOwner { - 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 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}"`); if (event !== Events.BrowserContext.Close) waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed')); diff --git a/src/client/channelOwner.ts b/src/client/channelOwner.ts index 9f437786d3..90cb40e68d 100644 --- a/src/client/channelOwner.ts +++ b/src/client/channelOwner.ts @@ -107,18 +107,6 @@ export abstract class ChannelOwner {}); - } - - _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() { // Jest's expect library tries to print objects sometimes. // RPC objects can contain links to lots of other objects, diff --git a/src/client/electron.ts b/src/client/electron.ts index 48e115a8e0..c49399cb24 100644 --- a/src/client/electron.ts +++ b/src/client/electron.ts @@ -18,7 +18,6 @@ import type { BrowserWindow } from 'electron'; import * as structs from '../../types/structs'; import * as api from '../../types/types'; import * as channels from '../protocol/channels'; -import { ParsedStackTrace } from '../utils/stackTrace'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { headersObjectToArray } from '../utils/utils'; import { BrowserContext } from './browserContext'; @@ -101,16 +100,16 @@ export class ElectronApplication extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => { await channel.close(); }); } async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise { - 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 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}"`); if (event !== Events.ElectronApplication.Close) waiter.rejectOnEvent(this, Events.ElectronApplication.Close, new Error('Electron application closed')); diff --git a/src/client/frame.ts b/src/client/frame.ts index 8b4f3ec029..1a9ac172b5 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -30,7 +30,6 @@ import { LifecycleEvent, URLMatch, SelectOption, SelectOptionOptions, FilePayloa import { urlMatches } from './clientHelper'; import * as api from '../../types/types'; import * as structs from '../../types/structs'; -import { ParsedStackTrace } from '../utils/stackTrace'; export type WaitForNavigationOptions = { timeout?: number, @@ -94,8 +93,8 @@ export class Frame extends ChannelOwner { - 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 waiter = this._setupNavigationWaiter(options, stackTrace); + const waiter = this._setupNavigationWaiter(options); const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`); @@ -145,8 +144,8 @@ export class Frame extends ChannelOwner { - const waiter = this._setupNavigationWaiter(options, stackTrace); + return this._wrapApiCall(async (channel: channels.FrameChannel) => { + const waiter = this._setupNavigationWaiter(options); await waiter.waitForEvent(this._eventEmitter, 'loadstate', s => { waiter.log(` "${s}" event fired`); return s === state; diff --git a/src/client/network.ts b/src/client/network.ts index c5c43db6a3..1955582adc 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -26,7 +26,6 @@ import { Events } from './events'; import { Page } from './page'; import { Waiter } from './waiter'; import * as api from '../../types/types'; -import { ParsedStackTrace } from '../utils/stackTrace'; export type NetworkCookie = { name: string, @@ -476,10 +475,10 @@ export class WebSocket extends ChannelOwner { - 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 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}"`); if (event !== Events.WebSocket.Error) waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error')); diff --git a/src/client/page.ts b/src/client/page.ts index 91b09c264c..93c0c369e9 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -46,7 +46,6 @@ import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } fro import { isSafeCloseError } from '../utils/errors'; import { Video } from './video'; import { Artifact } from './artifact'; -import { ParsedStackTrace } from '../utils/stackTrace'; type PDFOptions = Omit & { width?: string | number, @@ -349,7 +348,7 @@ export class Page extends ChannelOwner boolean | Promise), options: { timeout?: number } = {}): Promise { - return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const predicate = (request: Request) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) return urlMatches(request.url(), urlOrPredicate); @@ -357,12 +356,12 @@ export class Page extends ChannelOwner boolean | Promise), options: { timeout?: number } = {}): Promise { - return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const predicate = (response: Response) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) return urlMatches(response.url(), urlOrPredicate); @@ -370,20 +369,20 @@ export class Page extends ChannelOwner { - return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { - return this._waitForEvent(event, optionsOrPredicate, stackTrace, `waiting for event "${event}"`); + return this._wrapApiCall(async (channel: channels.PageChannel) => { + return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`); }); } - private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, stackTrace: ParsedStackTrace, logLine?: string): Promise { + private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise { const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event, stackTrace); + const waiter = Waiter.createForEvent(this, event); if (logLine) waiter.log(logLine); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); diff --git a/src/client/waiter.ts b/src/client/waiter.ts index 9adbd2cfd9..b240055741 100644 --- a/src/client/waiter.ts +++ b/src/client/waiter.ts @@ -15,9 +15,10 @@ */ import { EventEmitter } from 'events'; -import { ParsedStackTrace, rewriteErrorMessage } from '../utils/stackTrace'; +import { rewriteErrorMessage } from '../utils/stackTrace'; import { TimeoutError } from '../utils/errors'; import { createGuid } from '../utils/utils'; +import * as channels from '../protocol/channels'; import { ChannelOwner } from './channelOwner'; export class Waiter { @@ -26,21 +27,23 @@ export class Waiter { private _immediateError?: Error; // TODO: can/should we move these logs into wrapApiCall? private _logs: string[] = []; - private _channelOwner: ChannelOwner; + private _channel: channels.EventTargetChannel; private _waitId: string; private _error: string | undefined; - constructor(channelOwner: ChannelOwner, event: string, stackTrace: ParsedStackTrace) { + constructor(channelOwner: ChannelOwner, event: string) { this._waitId = createGuid(); - this._channelOwner = channelOwner; - this._channelOwner._waitForEventInfoBefore(this._waitId, event, stackTrace); + this._channel = channelOwner._channel; + channelOwner._wrapApiCall(async (channel: channels.EventTargetChannel) => { + channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {}); + }); 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) { - return new Waiter(channelOwner, event, stackTrace); + static createForEvent(channelOwner: ChannelOwner, event: string) { + return new Waiter(channelOwner, event); } async waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise): Promise { @@ -89,7 +92,7 @@ export class Waiter { log(s: string) { 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, dispose?: () => void) { diff --git a/src/dispatchers/dispatcher.ts b/src/dispatchers/dispatcher.ts index 055a6096d9..35d88313c6 100644 --- a/src/dispatchers/dispatcher.ts +++ b/src/dispatchers/dispatcher.ts @@ -77,7 +77,7 @@ export class Dispatcher extends Even (object as any)[dispatcherSymbol] = this; 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 = {}) { @@ -142,8 +142,8 @@ export class DispatcherConnection { const eventMetadata: CallMetadata = { id: `event@${++lastEventId}`, objectId: sdkObject?.guid, - pageId: sdkObject?.attribution.page?.guid, - frameId: sdkObject?.attribution.frame?.guid, + pageId: sdkObject?.attribution?.page?.guid, + frameId: sdkObject?.attribution?.frame?.guid, startTime: monotonicTime(), endTime: 0, type, @@ -152,7 +152,7 @@ export class DispatcherConnection { log: [], snapshots: [] }; - sdkObject.instrumentation.onEvent(sdkObject, eventMetadata); + sdkObject.instrumentation?.onEvent(sdkObject, eventMetadata); } this.onmessage({ guid, method, params }); } @@ -176,8 +176,6 @@ export class DispatcherConnection { }; const scheme = createScheme(tChannel); 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'; if (!scheme[name]) throw new ValidationError(`Unknown scheme for ${type}.${method}`); @@ -221,8 +219,8 @@ export class DispatcherConnection { id: `call@${id}`, ...validMetadata, objectId: sdkObject?.guid, - pageId: sdkObject?.attribution.page?.guid, - frameId: sdkObject?.attribution.frame?.guid, + pageId: sdkObject?.attribution?.page?.guid, + frameId: sdkObject?.attribution?.frame?.guid, startTime: monotonicTime(), endTime: 0, type: dispatcher._type, diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 691d0f3b25..f8e1372588 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -35,14 +35,6 @@ export type Metadata = { apiName?: string, }; -export type WaitForEventInfo = { - waitId: string, - phase: 'before' | 'after' | 'log', - event?: string, - message?: string, - error?: string, -}; - export type Point = { x: number, y: number, @@ -604,11 +596,30 @@ export type BrowserStopTracingResult = { binary: Binary, }; +// ----------- EventTarget ----------- +export type EventTargetInitializer = {}; +export interface EventTargetChannel extends Channel { + waitForEventInfo(params: EventTargetWaitForEventInfoParams, metadata?: Metadata): Promise; +} +export type EventTargetWaitForEventInfoParams = { + info: { + waitId: string, + phase: 'before' | 'after' | 'log', + event?: string, + message?: string, + error?: string, + }, +}; +export type EventTargetWaitForEventInfoOptions = { + +}; +export type EventTargetWaitForEventInfoResult = void; + // ----------- BrowserContext ----------- export type BrowserContextInitializer = { isChromium: boolean, }; -export interface BrowserContextChannel extends Channel { +export interface BrowserContextChannel extends EventTargetChannel { on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this; on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; @@ -868,7 +879,7 @@ export type PageInitializer = { isClosed: boolean, opener?: PageChannel, }; -export interface PageChannel extends Channel { +export interface PageChannel extends EventTargetChannel { on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => void): this; on(event: 'close', callback: (params: PageCloseEvent) => void): this; on(event: 'console', callback: (params: PageConsoleEvent) => void): this; @@ -2460,7 +2471,7 @@ export type RemoteAddr = { export type WebSocketInitializer = { url: string, }; -export interface WebSocketChannel extends Channel { +export interface WebSocketChannel extends EventTargetChannel { on(event: 'open', callback: (params: WebSocketOpenEvent) => void): this; on(event: 'frameSent', callback: (params: WebSocketFrameSentEvent) => void): this; on(event: 'frameReceived', callback: (params: WebSocketFrameReceivedEvent) => void): this; @@ -2717,7 +2728,7 @@ export type ElectronLaunchResult = { export type ElectronApplicationInitializer = { context: BrowserContextChannel, }; -export interface ElectronApplicationChannel extends Channel { +export interface ElectronApplicationChannel extends EventTargetChannel { on(event: 'close', callback: (params: ElectronApplicationCloseEvent) => void): this; browserWindow(params: ElectronApplicationBrowserWindowParams, metadata?: Metadata): Promise; evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, metadata?: Metadata): Promise; @@ -2807,7 +2818,7 @@ export type AndroidDeviceInitializer = { model: string, serial: string, }; -export interface AndroidDeviceChannel extends Channel { +export interface AndroidDeviceChannel extends EventTargetChannel { on(event: 'webViewAdded', callback: (params: AndroidDeviceWebViewAddedEvent) => void): this; on(event: 'webViewRemoved', callback: (params: AndroidDeviceWebViewRemovedEvent) => void): this; wait(params: AndroidDeviceWaitParams, metadata?: Metadata): Promise; @@ -3237,6 +3248,12 @@ export type SocksSocketEndOptions = {}; export type SocksSocketEndResult = void; export const commandsWithTracingSnapshots = new Set([ + 'EventTarget.waitForEventInfo', + 'BrowserContext.waitForEventInfo', + 'Page.waitForEventInfo', + 'WebSocket.waitForEventInfo', + 'ElectronApplication.waitForEventInfo', + 'AndroidDevice.waitForEventInfo', 'Page.goBack', 'Page.goForward', 'Page.reload', @@ -3285,7 +3302,9 @@ export const commandsWithTracingSnapshots = new Set([ 'Frame.waitForFunction', 'Frame.waitForSelector', 'JSHandle.evaluateExpression', + 'ElementHandle.evaluateExpression', 'JSHandle.evaluateExpressionHandle', + 'ElementHandle.evaluateExpressionHandle', 'ElementHandle.evalOnSelector', 'ElementHandle.evalOnSelectorAll', 'ElementHandle.check', diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 708b40d2ad..e4999a8770 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -31,21 +31,6 @@ Metadata: apiName: string? -WaitForEventInfo: - type: object - properties: - waitId: string - phase: - type: enum - literals: - - before - - after - - log - event: string? - message: string? - error: string? - - Point: type: object properties: @@ -504,10 +489,33 @@ Browser: 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: type: interface + extends: EventTarget + initializer: isChromium: boolean @@ -691,6 +699,8 @@ BrowserContext: Page: type: interface + extends: EventTarget + initializer: mainFrame: Frame viewportSize: @@ -2128,6 +2138,8 @@ RemoteAddr: WebSocket: type: interface + extends: EventTarget + initializer: url: string @@ -2346,6 +2358,8 @@ Electron: ElectronApplication: type: interface + extends: EventTarget + initializer: context: BrowserContext @@ -2413,6 +2427,8 @@ AndroidSocket: AndroidDevice: type: interface + extends: EventTarget + initializer: model: string serial: string diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 8c00d94a10..9491f90e26 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -43,13 +43,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { stack: tOptional(tArray(tType('StackFrame'))), 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({ x: tNumber, y: tNumber, @@ -335,6 +328,20 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { categories: tOptional(tArray(tString)), }); 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({ cookies: tArray(tType('SetNetworkCookie')), }); diff --git a/src/server/trace/recorder/tracing.ts b/src/server/trace/recorder/tracing.ts index 9e90a71a04..3fd7b7ea48 100644 --- a/src/server/trace/recorder/tracing.ts +++ b/src/server/trace/recorder/tracing.ts @@ -133,7 +133,7 @@ export class Tracing implements InstrumentationListener { return; if (!this._snapshotter.started()) return; - if (!this._shouldCaptureSnapshot(metadata)) + if (!shouldCaptureSnapshot(metadata)) return; const snapshotName = `${name}@${metadata.id}`; metadata.snapshots.push({ title: name, snapshotName }); @@ -162,7 +162,7 @@ export class Tracing implements InstrumentationListener { } pendingCall.afterSnapshot = this._captureSnapshot('after', sdkObject, metadata); 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._pendingCalls.delete(metadata.id); } @@ -225,8 +225,8 @@ export class Tracing implements InstrumentationListener { await fs.promises.appendFile(this._traceFile!, JSON.stringify(event) + '\n'); }); } - - private _shouldCaptureSnapshot(metadata: CallMetadata): boolean { - return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method); - } +} + +function shouldCaptureSnapshot(metadata: CallMetadata): boolean { + return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method); } diff --git a/src/server/trace/viewer/traceModel.ts b/src/server/trace/viewer/traceModel.ts index be96f90cf5..6407764c76 100644 --- a/src/server/trace/viewer/traceModel.ts +++ b/src/server/trace/viewer/traceModel.ts @@ -43,9 +43,8 @@ export class TraceModel { appendEvents(events: trace.TraceEvent[], snapshotStorage: SnapshotStorage) { for (const event of events) this.appendEvent(event); - const actions: trace.ActionTraceEvent[] = []; 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(); } @@ -55,6 +54,7 @@ export class TraceModel { pageEntry = { actions: [], events: [], + objects: {}, screencastFrames: [], }; this.pageEntries.set(pageId, pageEntry); @@ -83,8 +83,12 @@ export class TraceModel { } case 'event': { const metadata = event.metadata; - if (metadata.pageId) - this._pageEntry(metadata.pageId).events.push(event); + 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); + } break; } case 'resource-snapshot': @@ -113,6 +117,7 @@ export type ContextEntry = { export type PageEntry = { actions: trace.ActionTraceEvent[]; events: trace.ActionTraceEvent[]; + objects: { [ket: string]: any }; screencastFrames: { sha1: string, timestamp: number, diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index 45fa9209e1..7d26ff21d3 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -141,7 +141,10 @@ export class TraceViewer { }); await context.extendInjectedScript('main', consoleApiSource.source); const [page] = context.pages(); - page.on('close', () => context.close(internalCallMetadata()).catch(() => {})); + if (isUnderTest()) + page.on('close', () => context.close(internalCallMetadata()).catch(() => {})); + else + page.on('close', () => process.exit()); await page.mainFrame().goto(internalCallMetadata(), urlPrefix + '/traceviewer/traceViewer/index.html'); return context; } diff --git a/src/web/third_party/vscode/codicon.css b/src/web/third_party/vscode/codicon.css index 2e74c00a73..4cc718af34 100644 --- a/src/web/third_party/vscode/codicon.css +++ b/src/web/third_party/vscode/codicon.css @@ -22,6 +22,7 @@ user-select: none; } +.codicon-blank:before { content: '\81'; } .codicon-add:before { content: '\ea60'; } .codicon-plus:before { content: '\ea60'; } .codicon-gist-new:before { content: '\ea60'; } diff --git a/src/web/traceViewer/ui/filmStrip.tsx b/src/web/traceViewer/ui/filmStrip.tsx index 711ad7769c..b9d7b75cb8 100644 --- a/src/web/traceViewer/ui/filmStrip.tsx +++ b/src/web/traceViewer/ui/filmStrip.tsx @@ -42,7 +42,7 @@ export const FilmStrip: React.FunctionComponent<{ if (previewPoint !== undefined && screencastFrames) { const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewPoint.x / measure.width; 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
{ diff --git a/src/web/traceViewer/ui/tabbedPane.css b/src/web/traceViewer/ui/tabbedPane.css index e38ca83334..bd8f5edd10 100644 --- a/src/web/traceViewer/ui/tabbedPane.css +++ b/src/web/traceViewer/ui/tabbedPane.css @@ -44,7 +44,7 @@ } .tab-element { - padding: 2px 6px 0 6px; + padding: 2px 12px 0 12px; margin-right: 4px; cursor: pointer; display: flex; @@ -53,7 +53,6 @@ justify-content: center; user-select: none; border-bottom: 3px solid transparent; - width: 80px; outline: none; height: 100%; } diff --git a/src/web/traceViewer/ui/timeline.tsx b/src/web/traceViewer/ui/timeline.tsx index ad8651a163..68e271e8d9 100644 --- a/src/web/traceViewer/ui/timeline.tsx +++ b/src/web/traceViewer/ui/timeline.tsx @@ -170,8 +170,8 @@ export const Timeline: React.FunctionComponent<{ return
{bar.label} diff --git a/src/web/traceViewer/ui/workbench.tsx b/src/web/traceViewer/ui/workbench.tsx index a821dd5b16..b44cdc716d 100644 --- a/src/web/traceViewer/ui/workbench.tsx +++ b/src/web/traceViewer/ui/workbench.tsx @@ -54,6 +54,9 @@ export const Workbench: React.FunctionComponent<{ const snapshotSize = context.options.viewport || { width: 1280, height: 720 }; 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
🎭
diff --git a/tests/chromium/trace-viewer/trace-viewer.spec.ts b/tests/chromium/trace-viewer/trace-viewer.spec.ts index 08bf703a08..5414a4e919 100644 --- a/tests/chromium/trace-viewer/trace-viewer.spec.ts +++ b/tests/chromium/trace-viewer/trace-viewer.spec.ts @@ -24,8 +24,8 @@ class TraceViewerPage { constructor(public page: Page) {} async actionTitles() { - await this.page.waitForSelector('.action-title'); - return await this.page.$$eval('.action-title', ee => ee.map(e => e.textContent)); + await this.page.waitForSelector('.action-title:visible'); + return await this.page.$$eval('.action-title:visible', ee => ee.map(e => e.textContent)); } async selectAction(title: string) { @@ -33,7 +33,20 @@ class TraceViewerPage { } 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(); + 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,Hello world'); await page.setContent(''); await page.click('"Click"'); + await Promise.all([ + page.waitForNavigation(), + page.waitForTimeout(200).then(() => page.goto('data:text/html,Hello world 2')) + ]); + await page.evaluate(() => { + console.log('Log'); + console.warn('Warning'); + console.error('Error'); + }); await page.close(); traceFile = path.join(workerInfo.project.outputDir, 'trace.zip'); 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 }) => { 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 }) => { @@ -84,3 +113,9 @@ test('should contain action log', async ({ showTraceViewer }) => { expect(logLines).toContain('attempting click action'); 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'); +}); diff --git a/utils/generate_channels.js b/utils/generate_channels.js index c22c293b80..04c442fac2 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -187,6 +187,18 @@ for (const [name, value] of Object.entries(protocol)) { 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)) { if (item.type === 'interface') { const channelName = name; @@ -210,8 +222,11 @@ for (const [name, item] of Object.entries(protocol)) { for (let [methodName, method] of Object.entries(item.commands || {})) { if (method === null) method = {}; - if (method.tracing && method.tracing.snapshot) + if (method.tracing && method.tracing.snapshot) { tracingSnapshots.push(name + '.' + methodName); + for (const derived of derivedClasses.get(name) || []) + tracingSnapshots.push(derived + '.' + methodName); + } const parameters = objectType(method.parameters || {}, ''); const paramsName = `${channelName}${titleCase(methodName)}Params`; const optionsName = `${channelName}${titleCase(methodName)}Options`;