diff --git a/src/client/android.ts b/src/client/android.ts index f1e5b4a6901cb..3afc651a8d3ce 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -239,7 +239,7 @@ export class AndroidDevice extends ChannelOwner { const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event); + const waiter = Waiter.createForEvent(this, 'androidDevice', 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 69c8bb922a647..957d4dae93b7f 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -217,7 +217,7 @@ export class BrowserContext extends ChannelOwner { const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event); + const waiter = Waiter.createForEvent(this, 'browserContext', 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')); @@ -256,12 +256,6 @@ export class BrowserContext extends ChannelOwner { - await channel.pause(); - }); - } - async _enableRecorder(params: { language: string, launchOptions?: LaunchOptions, diff --git a/src/client/channelOwner.ts b/src/client/channelOwner.ts index a57e32352d47b..6a575687deac6 100644 --- a/src/client/channelOwner.ts +++ b/src/client/channelOwner.ts @@ -48,20 +48,7 @@ export abstract class ChannelOwner { - if (prop === 'debugScopeState') - return (params: any) => this._connection.sendMessageToServer(guid, prop, params); - if (typeof prop === 'string') { - const validator = scheme[paramsName(type, prop)]; - if (validator) - return (params: any) => this._connection.sendMessageToServer(guid, prop, validator(params, '')); - } - return obj[prop]; - }, - }); - (this._channel as any)._object = this; + this._channel = this._createChannel(new EventEmitter(), ''); this._initializer = initializer; } @@ -84,11 +71,29 @@ export abstract class ChannelOwner(apiName: string, func: (channel: K) => Promise, logger?: Logger): Promise { + _createChannel(base: Object, apiName: string): T { + const channel = new Proxy(base, { + get: (obj: any, prop) => { + if (prop === 'debugScopeState') + return (params: any) => this._connection.sendMessageToServer(this._guid, prop, params, apiName); + if (typeof prop === 'string') { + const validator = scheme[paramsName(this._type, prop)]; + if (validator) + return (params: any) => this._connection.sendMessageToServer(this._guid, prop, validator(params, ''), apiName); + } + return obj[prop]; + }, + }); + (channel as any)._object = this; + return channel; + } + + async _wrapApiCall(apiName: string, func: (channel: C) => Promise, logger?: Logger): Promise { logger = logger || this._logger; try { logApiCall(logger, `=> ${apiName} started`); - const result = await func(this._channel as any); + const channel = this._createChannel({}, apiName); + const result = await func(channel as any); logApiCall(logger, `<= ${apiName} succeeded`); return result; } catch (e) { @@ -99,15 +104,15 @@ export abstract class ChannelOwner {}); + this._connection.sendMessageToServer(this._guid, 'waitForEventInfo', { info: { name, waitId, phase: 'before', stack } }, undefined).catch(() => {}); } _waitForEventInfoAfter(waitId: string, error?: string) { - this._connection.sendMessageToServer(this._guid, 'waitForEventInfo', { info: { waitId, phase: 'after', error } }).catch(() => {}); + this._connection.sendMessageToServer(this._guid, 'waitForEventInfo', { info: { waitId, phase: 'after', error } }, undefined).catch(() => {}); } _waitForEventInfoLog(waitId: string, message: string) { - this._connection.sendMessageToServer(this._guid, 'waitForEventInfo', { info: { waitId, phase: 'log', message } }).catch(() => {}); + this._connection.sendMessageToServer(this._guid, 'waitForEventInfo', { info: { waitId, phase: 'log', message } }, undefined).catch(() => {}); } private toJSON() { diff --git a/src/client/connection.ts b/src/client/connection.ts index 6a70f44ed9bc9..c001c60e31af7 100644 --- a/src/client/connection.ts +++ b/src/client/connection.ts @@ -71,13 +71,13 @@ export class Connection { return this._objects.get(guid)!; } - async sendMessageToServer(guid: string, method: string, params: any): Promise { + async sendMessageToServer(guid: string, method: string, params: any, apiName: string | undefined): Promise { const { stack, frames } = captureStackTrace(); const id = ++this._lastId; const converted = { id, guid, method, params }; // Do not include metadata in debug logs to avoid noise. debugLogger.log('channel:command', converted); - this.onmessage({ ...converted, metadata: { stack: frames } }); + this.onmessage({ ...converted, metadata: { stack: frames, apiName } }); try { return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject })); } catch (e) { diff --git a/src/client/electron.ts b/src/client/electron.ts index 6311c03d83aff..2be69fcd5ce06 100644 --- a/src/client/electron.ts +++ b/src/client/electron.ts @@ -101,7 +101,7 @@ export class ElectronApplication extends ChannelOwner { const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event); + const waiter = Waiter.createForEvent(this, 'electronApplication', 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 42112962d5536..9ca8275a1b90d 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -113,7 +113,7 @@ export class Frame extends ChannelOwner { return this._wrapApiCall(this._apiName('waitForNavigation'), async (channel: channels.FrameChannel) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); - const waiter = this._setupNavigationWaiter('waitForNavigation', options); + const waiter = this._setupNavigationWaiter(this._apiName('waitForNavigation'), options); const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`); @@ -150,7 +150,7 @@ export class Frame extends ChannelOwner { - const waiter = this._setupNavigationWaiter('waitForLoadState', options); + const waiter = this._setupNavigationWaiter(this._apiName('waitForLoadState'), 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 a118328b6daea..44c96dc7263d7 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -365,7 +365,7 @@ export class WebSocket extends ChannelOwner { const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event); + const waiter = Waiter.createForEvent(this, 'webSocket', 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 b508ebfe40c46..882510baae710 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -397,7 +397,7 @@ export class Page extends ChannelOwner { const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this, event); + const waiter = Waiter.createForEvent(this, 'page', event); if (logLine) waiter.log(logLine); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); @@ -644,7 +644,9 @@ export class Page extends ChannelOwner { + await channel.pause(); + }); } async _pdf(options: PDFOptions = {}): Promise { diff --git a/src/client/waiter.ts b/src/client/waiter.ts index 7dc038577c380..b5f43d91205e5 100644 --- a/src/client/waiter.ts +++ b/src/client/waiter.ts @@ -38,8 +38,8 @@ export class Waiter { ]; } - static createForEvent(channelOwner: ChannelOwner, event: string) { - return new Waiter(channelOwner, `waitForEvent(${event})`); + static createForEvent(channelOwner: ChannelOwner, target: string, event: string) { + return new Waiter(channelOwner, `${target}.waitForEvent(${event})`); } async waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean): Promise { diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 03f175e5ad559..fe43f805cdfbd 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -32,6 +32,7 @@ export type StackFrame = { export type Metadata = { stack?: StackFrame[], + apiName?: string, }; export type WaitForEventInfo = { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index dba0a267a787b..2bb038548ce01 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -28,6 +28,7 @@ Metadata: stack: type: array? items: StackFrame + apiName: string? WaitForEventInfo: diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index d69edaeb12154..7c21783fb9e49 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -41,6 +41,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.Metadata = tObject({ stack: tOptional(tArray(tType('StackFrame'))), + apiName: tOptional(tString), }); scheme.WaitForEventInfo = tObject({ waitId: tString, diff --git a/src/server/instrumentation.ts b/src/server/instrumentation.ts index bb4165cd59b74..e15abf77679bb 100644 --- a/src/server/instrumentation.ts +++ b/src/server/instrumentation.ts @@ -39,6 +39,7 @@ export type CallMetadata = { type: string; method: string; params: any; + apiName?: string; stack?: StackFrame[]; log: string[]; error?: string; diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 5e0e9f729ff3e..a39ef97282486 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -434,7 +434,7 @@ export class RecorderSupplement { for (const metadata of metadatas) { if (!metadata.method) continue; - const title = metadata.method; + const title = metadata.apiName || metadata.method; let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done'; if (this._currentCallsMetadata.has(metadata)) status = 'in-progress'; @@ -452,7 +452,8 @@ export class RecorderSupplement { logs.push({ id: metadata.id, messages: metadata.log, - title, status, + title, + status, error: metadata.error, params, duration diff --git a/test/pause.spec.ts b/test/pause.spec.ts index 609ee0b39573c..d9140b327e8f5 100644 --- a/test/pause.spec.ts +++ b/test/pause.spec.ts @@ -155,9 +155,9 @@ describe('pause', (suite, { mode }) => { await recorderPage.click('[title="Resume"]'); await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")'); expect(await sanitizeLog(recorderPage)).toEqual([ - 'pause- XXms', - 'click(button)- XXms', - 'pause', + 'page.pause- XXms', + 'page.click(button)- XXms', + 'page.pause', ]); await recorderPage.click('[title="Resume"]'); await scriptPromise; @@ -177,10 +177,10 @@ describe('pause', (suite, { mode }) => { await recorderPage.click('[title="Resume"]'); await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")'); expect(await sanitizeLog(recorderPage)).toEqual([ - 'pause- XXms', - 'waitForEvent(console)- XXms', - 'click(button)- XXms', - 'pause', + 'page.pause- XXms', + 'page.waitForEvent(console)- XXms', + 'page.click(button)- XXms', + 'page.pause', ]); await recorderPage.click('[title="Resume"]'); await scriptPromise; @@ -196,8 +196,8 @@ describe('pause', (suite, { mode }) => { await recorderPage.click('[title="Resume"]'); await recorderPage.waitForSelector('.source-line-error'); expect(await sanitizeLog(recorderPage)).toEqual([ - 'pause- XXms', - 'isChecked(button)- XXms', + 'page.pause- XXms', + 'page.isChecked(button)- XXms', 'checking \"checked\" state of \"button\"', 'selector resolved to ', 'Not a checkbox or radio button',