diff --git a/.gitignore b/.gitignore index 468623158e..40bc2f31c5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ drivers/ .gradle/ nohup.out .trace +.tmp \ No newline at end of file diff --git a/src/client/android.ts b/src/client/android.ts index c5cc68d55a..ad33a3bdbf 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -26,6 +26,7 @@ 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 }; @@ -48,7 +49,7 @@ export class Android extends ChannelOwner { - return this._wrapApiCall('android.devices', async (channel: channels.AndroidChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidChannel) => { const { devices } = await channel.devices(); return devices.map(d => AndroidDevice.from(d)); }); @@ -114,13 +115,13 @@ export class AndroidDevice extends ChannelOwner { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.wait({ selector: toSelectorChannel(selector), ...options }); }); } async fill(selector: api.AndroidSelector, text: string, options?: types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.fill', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.fill({ selector: toSelectorChannel(selector), text, ...options }); }); } @@ -131,61 +132,61 @@ export class AndroidDevice extends ChannelOwner { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.tap({ selector: toSelectorChannel(selector), ...options }); }); } async drag(selector: api.AndroidSelector, dest: types.Point, options?: SpeedOptions & types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.drag', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.drag({ selector: toSelectorChannel(selector), dest, ...options }); }); } async fling(selector: api.AndroidSelector, direction: Direction, options?: SpeedOptions & types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.fling', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.fling({ selector: toSelectorChannel(selector), direction, ...options }); }); } async longTap(selector: api.AndroidSelector, options?: types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.longTap', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.longTap({ selector: toSelectorChannel(selector), ...options }); }); } async pinchClose(selector: api.AndroidSelector, percent: number, options?: SpeedOptions & types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.pinchClose', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.pinchClose({ selector: toSelectorChannel(selector), percent, ...options }); }); } async pinchOpen(selector: api.AndroidSelector, percent: number, options?: SpeedOptions & types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.pinchOpen', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.pinchOpen({ selector: toSelectorChannel(selector), percent, ...options }); }); } async scroll(selector: api.AndroidSelector, direction: Direction, percent: number, options?: SpeedOptions & types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.scroll', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.scroll({ selector: toSelectorChannel(selector), direction, percent, ...options }); }); } async swipe(selector: api.AndroidSelector, direction: Direction, percent: number, options?: SpeedOptions & types.TimeoutOptions) { - await this._wrapApiCall('androidDevice.swipe', async (channel: channels.AndroidDeviceChannel) => { + await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.swipe({ selector: toSelectorChannel(selector), direction, percent, ...options }); }); } async info(selector: api.AndroidSelector): Promise { - return await this._wrapApiCall('androidDevice.info', async (channel: channels.AndroidDeviceChannel) => { + return await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { return (await channel.info({ selector: toSelectorChannel(selector) })).info; }); } async screenshot(options: { path?: string } = {}): Promise { - return await this._wrapApiCall('androidDevice.screenshot', async (channel: channels.AndroidDeviceChannel) => { + return await this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { const { binary } = await channel.screenshot(); const buffer = Buffer.from(binary, 'base64'); if (options.path) @@ -195,39 +196,39 @@ export class AndroidDevice extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.close(); this.emit(Events.AndroidDevice.Close); }); } async shell(command: string): Promise { - return this._wrapApiCall('androidDevice.shell', async (channel: channels.AndroidDeviceChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { const { result } = await channel.shell({ command }); return Buffer.from(result, 'base64'); }); } async open(command: string): Promise { - return this._wrapApiCall('androidDevice.open', async (channel: channels.AndroidDeviceChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { return AndroidSocket.from((await channel.open({ command })).socket); }); } async installApk(file: string | Buffer, options?: { args: string[] }): Promise { - return this._wrapApiCall('androidDevice.installApk', async (channel: channels.AndroidDeviceChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.installApk({ file: await loadFile(file), args: options && options.args }); }); } async push(file: string | Buffer, path: string, options?: { mode: number }): Promise { - return this._wrapApiCall('androidDevice.push', async (channel: channels.AndroidDeviceChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined }); }); } async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise { - return this._wrapApiCall('androidDevice.launchBrowser', async (channel: channels.AndroidDeviceChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { const contextOptions = await prepareBrowserContextParams(options); const { context } = await channel.launchBrowser(contextOptions); return BrowserContext.from(context) as BrowserContext; @@ -235,15 +236,17 @@ 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, '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')); - const result = await waiter.waitForEvent(this, event, predicate as any); - waiter.dispose(); - return result; + return this._wrapApiCall(async (channel: channels.AndroidDeviceChannel, stackTrace: ParsedStackTrace) => { + const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); + const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; + const waiter = Waiter.createForEvent(this, event, stackTrace); + waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); + if (event !== Events.AndroidDevice.Close) + waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed')); + const result = await waiter.waitForEvent(this, event, predicate as any); + waiter.dispose(); + return result; + }); } } @@ -259,13 +262,13 @@ export class AndroidSocket extends ChannelOwner { - return this._wrapApiCall('androidDevice.write', async (channel: channels.AndroidSocketChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidSocketChannel) => { await channel.write({ data: data.toString('base64') }); }); } async close(): Promise { - return this._wrapApiCall('androidDevice.close', async (channel: channels.AndroidSocketChannel) => { + return this._wrapApiCall(async (channel: channels.AndroidSocketChannel) => { await channel.close(); }); } @@ -285,31 +288,31 @@ export class AndroidInput implements api.AndroidInput { } async type(text: string) { - return this._device._wrapApiCall('androidDevice.inputType', async (channel: channels.AndroidDeviceChannel) => { + return this._device._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.inputType({ text }); }); } async press(key: api.AndroidKey) { - return this._device._wrapApiCall('androidDevice.inputPress', async (channel: channels.AndroidDeviceChannel) => { + return this._device._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.inputPress({ key }); }); } async tap(point: types.Point) { - return this._device._wrapApiCall('androidDevice.inputTap', async (channel: channels.AndroidDeviceChannel) => { + return this._device._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.inputTap({ point }); }); } async swipe(from: types.Point, segments: types.Point[], steps: number) { - return this._device._wrapApiCall('androidDevice.inputSwipe', async (channel: channels.AndroidDeviceChannel) => { + return this._device._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.inputSwipe({ segments, steps }); }); } async drag(from: types.Point, to: types.Point, steps: number) { - return this._device._wrapApiCall('androidDevice.inputDragAndDrop', async (channel: channels.AndroidDeviceChannel) => { + return this._device._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { await channel.inputDrag({ from, to, steps }); }); } @@ -391,7 +394,7 @@ export class AndroidWebView extends EventEmitter implements api.AndroidWebView { } private async _fetchPage(): Promise { - return this._device._wrapApiCall('androidWebView.page', async (channel: channels.AndroidDeviceChannel) => { + return this._device._wrapApiCall(async (channel: channels.AndroidDeviceChannel) => { const { context } = await channel.connectToWebView({ pid: this._data.pid, sdkLanguage: 'javascript' }); return BrowserContext.from(context).pages()[0]; }); diff --git a/src/client/artifact.ts b/src/client/artifact.ts index 5291a34767..0eb0dc4cce 100644 --- a/src/client/artifact.ts +++ b/src/client/artifact.ts @@ -23,7 +23,6 @@ import { Readable } from 'stream'; export class Artifact extends ChannelOwner { _isRemote = false; - _apiName: string = ''; static from(channel: channels.ArtifactChannel): Artifact { return (channel as any)._object; @@ -32,13 +31,13 @@ export class Artifact extends ChannelOwner { if (this._isRemote) throw new Error(`Path is not available when using browserType.connect(). Use saveAs() to save a local copy.`); - return this._wrapApiCall(`${this._apiName}.path`, async (channel: channels.ArtifactChannel) => { + return this._wrapApiCall(async (channel: channels.ArtifactChannel) => { return (await channel.pathAfterFinished()).value || null; }); } async saveAs(path: string): Promise { - return this._wrapApiCall(`${this._apiName}.saveAs`, async (channel: channels.ArtifactChannel) => { + return this._wrapApiCall(async (channel: channels.ArtifactChannel) => { if (!this._isRemote) { await channel.saveAs({ path }); return; @@ -56,13 +55,13 @@ export class Artifact extends ChannelOwner { - return this._wrapApiCall(`${this._apiName}.failure`, async (channel: channels.ArtifactChannel) => { + return this._wrapApiCall(async (channel: channels.ArtifactChannel) => { return (await channel.failure()).error || null; }); } async createReadStream(): Promise { - return this._wrapApiCall(`${this._apiName}.createReadStream`, async (channel: channels.ArtifactChannel) => { + return this._wrapApiCall(async (channel: channels.ArtifactChannel) => { const result = await channel.stream(); if (!result.stream) return null; @@ -72,13 +71,13 @@ export class Artifact extends ChannelOwner { - return this._wrapApiCall(`${this._apiName}.cancel`, async (channel: channels.ArtifactChannel) => { + return this._wrapApiCall(async (channel: channels.ArtifactChannel) => { return channel.cancel(); }); } async delete(): Promise { - return this._wrapApiCall(`${this._apiName}.delete`, async (channel: channels.ArtifactChannel) => { + return this._wrapApiCall(async (channel: channels.ArtifactChannel) => { return channel.delete(); }); } diff --git a/src/client/browser.ts b/src/client/browser.ts index a059796372..8860420c2b 100644 --- a/src/client/browser.ts +++ b/src/client/browser.ts @@ -47,7 +47,7 @@ export class Browser extends ChannelOwner { - return this._wrapApiCall('browser.newContext', async (channel: channels.BrowserChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserChannel) => { const contextOptions = await prepareBrowserContextParams(options); const context = BrowserContext.from((await channel.newContext(contextOptions)).context); context._options = contextOptions; @@ -78,26 +78,26 @@ export class Browser extends ChannelOwner { - return this._wrapApiCall('browser.newBrowserCDPSession', async (channel: channels.BrowserChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserChannel) => { return CDPSession.from((await channel.newBrowserCDPSession()).session); }); } async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { - return this._wrapApiCall('browser.startTracing', async (channel: channels.BrowserChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserChannel) => { await channel.startTracing({ ...options, page: page ? page._channel : undefined }); }); } async stopTracing(): Promise { - return this._wrapApiCall('browser.stopTracing', async (channel: channels.BrowserChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserChannel) => { return Buffer.from((await channel.stopTracing()).binary, 'base64'); }); } async close(): Promise { try { - await this._wrapApiCall('browser.close', async (channel: channels.BrowserChannel) => { + await this._wrapApiCall(async (channel: channels.BrowserChannel) => { if (this._remoteType === 'owns-connection') this._connection.close(); else diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 0ea9f29a54..f9678d3cd8 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -33,6 +33,7 @@ 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(); @@ -162,7 +163,7 @@ export class BrowserContext extends ChannelOwner { - return this._wrapApiCall('browserContext.newPage', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { if (this._ownerPage) throw new Error('Please use browser.newContext()'); return Page.from((await channel.newPage()).page); @@ -174,50 +175,50 @@ export class BrowserContext extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { return (await channel.cookies({ urls: urls as string[] })).cookies; }); } async addCookies(cookies: network.SetNetworkCookieParam[]): Promise { - return this._wrapApiCall('browserContext.addCookies', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.addCookies({ cookies }); }); } async clearCookies(): Promise { - return this._wrapApiCall('browserContext.clearCookies', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.clearCookies(); }); } async grantPermissions(permissions: string[], options?: { origin?: string }): Promise { - return this._wrapApiCall('browserContext.grantPermissions', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.grantPermissions({ permissions, ...options }); }); } async clearPermissions(): Promise { - return this._wrapApiCall('browserContext.clearPermissions', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.clearPermissions(); }); } async setGeolocation(geolocation: { longitude: number, latitude: number, accuracy?: number } | null): Promise { - return this._wrapApiCall('browserContext.setGeolocation', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.setGeolocation({ geolocation: geolocation || undefined }); }); } async setExtraHTTPHeaders(headers: Headers): Promise { - return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { network.validateHeaders(headers); await channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); }); } async setOffline(offline: boolean): Promise { - return this._wrapApiCall('browserContext.setOffline', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.setOffline({ offline }); }); } @@ -225,27 +226,27 @@ export class BrowserContext extends ChannelOwner { if (!isUnderTest()) deprecate(`context.setHTTPCredentials`, `warning: method |context.setHTTPCredentials()| is deprecated. Instead of changing credentials, create another browser context with new credentials.`); - return this._wrapApiCall('browserContext.setHTTPCredentials', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.setHTTPCredentials({ httpCredentials: httpCredentials || undefined }); }); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { - return this._wrapApiCall('browserContext.addInitScript', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { const source = await evaluationScript(script, arg); await channel.addInitScript({ source }); }); } async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}): Promise { - return this._wrapApiCall('browserContext.exposeBinding', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.exposeBinding({ name, needsHandle: options.handle }); this._bindings.set(name, callback); }); } async exposeFunction(name: string, callback: Function): Promise { - return this._wrapApiCall('browserContext.exposeFunction', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.exposeBinding({ name }); const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args); this._bindings.set(name, binding); @@ -253,7 +254,7 @@ export class BrowserContext extends ChannelOwner { - return this._wrapApiCall('browserContext.route', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { this._routes.push({ url, handler }); if (this._routes.length === 1) await channel.setNetworkInterceptionEnabled({ enabled: true }); @@ -261,7 +262,7 @@ export class BrowserContext extends ChannelOwner { - return this._wrapApiCall('browserContext.unroute', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); if (this._routes.length === 0) await channel.setNetworkInterceptionEnabled({ enabled: false }); @@ -269,19 +270,21 @@ 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, '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')); - const result = await waiter.waitForEvent(this, event, predicate as any); - waiter.dispose(); - return result; + return this._wrapApiCall(async (channel: channels.BrowserContextChannel, stackTrace: ParsedStackTrace) => { + const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); + const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; + const waiter = Waiter.createForEvent(this, event, stackTrace); + waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); + if (event !== Events.BrowserContext.Close) + waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed')); + const result = await waiter.waitForEvent(this, event, predicate as any); + waiter.dispose(); + return result; + }); } async storageState(options: { path?: string } = {}): Promise { - return await this._wrapApiCall('browserContext.storageState', async (channel: channels.BrowserContextChannel) => { + return await this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { const state = await channel.storageState(); if (options.path) { await mkdirIfNeeded(options.path); @@ -300,7 +303,7 @@ export class BrowserContext extends ChannelOwner { - return this._wrapApiCall('browserContext.newCDPSession', async (channel: channels.BrowserContextChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { const result = await channel.newCDPSession({ page: page._channel }); return CDPSession.from(result.session); }); @@ -314,7 +317,7 @@ export class BrowserContext extends ChannelOwner { try { - await this._wrapApiCall('browserContext.close', async (channel: channels.BrowserContextChannel) => { + await this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.close(); await this._closedPromise; }); diff --git a/src/client/browserType.ts b/src/client/browserType.ts index 781be4076e..7bcb977da1 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -66,7 +66,7 @@ export class BrowserType extends ChannelOwner { const logger = options.logger; - return this._wrapApiCall('browserType.launch', async (channel: channels.BrowserTypeChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => { assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); const launchOptions: channels.BrowserTypeLaunchParams = { @@ -88,7 +88,7 @@ export class BrowserType extends ChannelOwner { - return this._wrapApiCall('browserType.launchPersistentContext', async (channel: channels.BrowserTypeChannel) => { + return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => { assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); const contextParams = await prepareBrowserContextParams(options); const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { @@ -110,7 +110,7 @@ export class BrowserType extends ChannelOwner { const logger = params.logger; const paramsHeaders = Object.assign({'User-Agent': getUserAgent()}, params.headers); - return this._wrapApiCall('browserType.connect', async () => { + return this._wrapApiCall(async () => { const ws = new WebSocket(params.wsEndpoint, [], { perMessageDeflate: false, maxPayload: 256 * 1024 * 1024, // 256Mb, @@ -221,7 +221,7 @@ export class BrowserType extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.BrowserTypeChannel) => { const paramsHeaders = Object.assign({'User-Agent': getUserAgent()}, params.headers); const headers = paramsHeaders ? headersObjectToArray(paramsHeaders) : undefined; const result = await channel.connectOverCDP({ diff --git a/src/client/cdpSession.ts b/src/client/cdpSession.ts index b8560a11dd..783d30be6a 100644 --- a/src/client/cdpSession.ts +++ b/src/client/cdpSession.ts @@ -42,14 +42,14 @@ export class CDPSession extends ChannelOwner { - return this._wrapApiCall('cdpSession.send', async (channel: channels.CDPSessionChannel) => { + return this._wrapApiCall(async (channel: channels.CDPSessionChannel) => { const result = await channel.send({ method, params }); return result.result as Protocol.CommandReturnValues[T]; }); } async detach() { - return this._wrapApiCall('cdpSession.detach', async (channel: channels.CDPSessionChannel) => { + return this._wrapApiCall(async (channel: channels.CDPSessionChannel) => { return channel.detach(); }); } diff --git a/src/client/channelOwner.ts b/src/client/channelOwner.ts index 66c9922193..9f437786d3 100644 --- a/src/client/channelOwner.ts +++ b/src/client/channelOwner.ts @@ -18,7 +18,8 @@ import { EventEmitter } from 'events'; import * as channels from '../protocol/channels'; import { createScheme, ValidationError, Validator } from '../protocol/validator'; import { debugLogger } from '../utils/debugLogger'; -import { rewriteErrorMessage } from '../utils/stackTrace'; +import { captureStackTrace, ParsedStackTrace } from '../utils/stackTrace'; +import { isUnderTest } from '../utils/utils'; import type { Connection } from './connection'; import type { Logger } from './types'; @@ -47,7 +48,7 @@ export abstract class ChannelOwner { if (prop === 'debugScopeState') - return (params: any) => this._connection.sendMessageToServer(this, prop, params, apiName); + return (params: any) => this._connection.sendMessageToServer(this, prop, params, stackTrace); if (typeof prop === 'string') { const validator = scheme[paramsName(this._type, prop)]; if (validator) - return (params: any) => this._connection.sendMessageToServer(this, prop, validator(params, ''), apiName); + return (params: any) => this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace); } return obj[prop]; }, @@ -87,31 +88,35 @@ export abstract class ChannelOwner(apiName: string, func: (channel: C) => Promise, logger?: Logger): Promise { + async _wrapApiCall(func: (channel: C, stackTrace: ParsedStackTrace) => Promise, logger?: Logger): Promise { logger = logger || this._logger; + const stackTrace = captureStackTrace(); + const { apiName, frameTexts } = stackTrace; + const channel = this._createChannel({}, stackTrace); try { logApiCall(logger, `=> ${apiName} started`); - const channel = this._createChannel({}, apiName); - const result = await func(channel as any); + const result = await func(channel as any, stackTrace); logApiCall(logger, `<= ${apiName} succeeded`); return result; } catch (e) { + const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n\n' + e.stack : ''; + e.message = apiName + ': ' + e.message; + e.stack = e.message + '\n' + frameTexts.join('\n') + innerError; logApiCall(logger, `<= ${apiName} failed`); - rewriteErrorMessage(e, `${apiName}: ` + e.message); throw e; } } - _waitForEventInfoBefore(waitId: string, apiName: string) { - this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { apiName, waitId, phase: 'before' } }, undefined).catch(() => {}); + _waitForEventInfoBefore(waitId: string, event: string, stackTrace: ParsedStackTrace) { + this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { waitId, phase: 'before', event } }, stackTrace).catch(() => {}); } - _waitForEventInfoAfter(waitId: string, error?: string) { - this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { waitId, phase: 'after', error } }, undefined).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 } }, undefined).catch(() => {}); + this._connection.sendMessageToServer(this, 'waitForEventInfo', { info: { waitId, phase: 'log', message } }, null).catch(() => {}); } private toJSON() { diff --git a/src/client/connection.ts b/src/client/connection.ts index eae714fcf5..665df975ee 100644 --- a/src/client/connection.ts +++ b/src/client/connection.ts @@ -34,10 +34,9 @@ import * as channels from '../protocol/channels'; import { Stream } from './stream'; import { debugLogger } from '../utils/debugLogger'; import { SelectorsOwner } from './selectors'; -import { isUnderTest } from '../utils/utils'; import { Android, AndroidSocket, AndroidDevice } from './android'; import { SocksSocket } from './socksSocket'; -import { captureStackTrace } from '../utils/stackTrace'; +import { ParsedStackTrace } from '../utils/stackTrace'; import { Artifact } from './artifact'; import { EventEmitter } from 'events'; @@ -77,24 +76,20 @@ export class Connection extends EventEmitter { return this._objects.get(guid)!; } - async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined): Promise { + async sendMessageToServer(object: ChannelOwner, method: string, params: any, stackTrace: ParsedStackTrace | null): Promise { const guid = object._guid; - const { stack, frames } = captureStackTrace(); + const { frames, apiName }: ParsedStackTrace = stackTrace || { frameTexts: [], frames: [], apiName: '' }; + 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); const metadata: channels.Metadata = { stack: frames, apiName }; this.onmessage({ ...converted, metadata }); - try { - if (this._disconnectedErrorMessage) - throw new Error(this._disconnectedErrorMessage); - return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, metadata })); - } catch (e) { - const innerStack = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? e.stack.substring(e.stack.indexOf(e.message) + e.message.length) : ''; - e.stack = e.message + innerStack + '\n' + stack; - throw e; - } + + if (this._disconnectedErrorMessage) + throw new Error(this._disconnectedErrorMessage); + return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, metadata })); } _debugScopeState(): any { diff --git a/src/client/dialog.ts b/src/client/dialog.ts index a0ccf5a39a..1fe6837d6f 100644 --- a/src/client/dialog.ts +++ b/src/client/dialog.ts @@ -40,13 +40,13 @@ export class Dialog extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.DialogChannel) => { await channel.accept({ promptText }); }); } async dismiss() { - return this._wrapApiCall('dialog.dismiss', async (channel: channels.DialogChannel) => { + return this._wrapApiCall(async (channel: channels.DialogChannel) => { await channel.dismiss(); }); } diff --git a/src/client/electron.ts b/src/client/electron.ts index 9233b125c6..48e115a8e0 100644 --- a/src/client/electron.ts +++ b/src/client/electron.ts @@ -18,6 +18,7 @@ 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'; @@ -46,7 +47,7 @@ export class Electron extends ChannelOwner { - return this._wrapApiCall('electron.launch', async (channel: channels.ElectronChannel) => { + return this._wrapApiCall(async (channel: channels.ElectronChannel) => { const params: channels.ElectronLaunchParams = { sdkLanguage: 'javascript', ...options, @@ -88,7 +89,7 @@ export class ElectronApplication extends ChannelOwner { - return this._wrapApiCall('electronApplication.firstWindow', async (channel: channels.ElectronApplicationChannel) => { + return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => { if (this._windows.size) return this._windows.values().next().value; return this.waitForEvent('window'); @@ -100,37 +101,41 @@ export class ElectronApplication extends ChannelOwner { + await channel.close(); + }); } async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise { - const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); - const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - 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')); - const result = await waiter.waitForEvent(this, event, predicate as any); - waiter.dispose(); - return result; + return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel, stackTrace: ParsedStackTrace) => { + const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); + const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; + const waiter = Waiter.createForEvent(this, event, stackTrace); + 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')); + const result = await waiter.waitForEvent(this, event, predicate as any); + waiter.dispose(); + return result; + }); } async browserWindow(page: Page): Promise> { - return this._wrapApiCall('electronApplication.browserWindow', async (channel: channels.ElectronApplicationChannel) => { + return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => { const result = await channel.browserWindow({ page: page._channel }); return JSHandle.from(result.handle); }); } async evaluate(pageFunction: structs.PageFunctionOn, arg: Arg): Promise { - return this._wrapApiCall('electronApplication.evaluate', async (channel: channels.ElectronApplicationChannel) => { + return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => { const result = await channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async evaluateHandle(pageFunction: structs.PageFunctionOn, arg: Arg): Promise> { - return this._wrapApiCall('electronApplication.evaluateHandle', async (channel: channels.ElectronApplicationChannel) => { + return this._wrapApiCall(async (channel: channels.ElectronApplicationChannel) => { const result = await channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return JSHandle.from(result.handle) as any as structs.SmartHandle; }); diff --git a/src/client/elementHandle.ts b/src/client/elementHandle.ts index 1a2200a9b2..d9494121eb 100644 --- a/src/client/elementHandle.ts +++ b/src/client/elementHandle.ts @@ -47,185 +47,185 @@ export class ElementHandle extends JSHandle implements } async ownerFrame(): Promise { - return this._wrapApiCall('elementHandle.ownerFrame', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return Frame.fromNullable((await channel.ownerFrame()).frame); }); } async contentFrame(): Promise { - return this._wrapApiCall('elementHandle.contentFrame', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return Frame.fromNullable((await channel.contentFrame()).frame); }); } async getAttribute(name: string): Promise { - return this._wrapApiCall('elementHandle.getAttribute', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const value = (await channel.getAttribute({ name })).value; return value === undefined ? null : value; }); } async inputValue(): Promise { - return this._wrapApiCall('elementHandle.inputValue', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.inputValue()).value; }); } async textContent(): Promise { - return this._wrapApiCall('elementHandle.textContent', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const value = (await channel.textContent()).value; return value === undefined ? null : value; }); } async innerText(): Promise { - return this._wrapApiCall('elementHandle.innerText', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.innerText()).value; }); } async innerHTML(): Promise { - return this._wrapApiCall('elementHandle.innerHTML', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.innerHTML()).value; }); } async isChecked(): Promise { - return this._wrapApiCall('elementHandle.isChecked', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.isChecked()).value; }); } async isDisabled(): Promise { - return this._wrapApiCall('elementHandle.isDisabled', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.isDisabled()).value; }); } async isEditable(): Promise { - return this._wrapApiCall('elementHandle.isEditable', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.isEditable()).value; }); } async isEnabled(): Promise { - return this._wrapApiCall('elementHandle.isEnabled', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.isEnabled()).value; }); } async isHidden(): Promise { - return this._wrapApiCall('elementHandle.isHidden', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.isHidden()).value; }); } async isVisible(): Promise { - return this._wrapApiCall('elementHandle.isVisible', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return (await channel.isVisible()).value; }); } async dispatchEvent(type: string, eventInit: Object = {}) { - return this._wrapApiCall('elementHandle.dispatchEvent', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.dispatchEvent({ type, eventInit: serializeArgument(eventInit) }); }); } async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) { - return this._wrapApiCall('elementHandle.scrollIntoViewIfNeeded', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.scrollIntoViewIfNeeded(options); }); } async hover(options: channels.ElementHandleHoverOptions = {}): Promise { - return this._wrapApiCall('elementHandle.hover', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.hover(options); }); } async click(options: channels.ElementHandleClickOptions = {}): Promise { - return this._wrapApiCall('elementHandle.click', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.click(options); }); } async dblclick(options: channels.ElementHandleDblclickOptions = {}): Promise { - return this._wrapApiCall('elementHandle.dblclick', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.dblclick(options); }); } async tap(options: channels.ElementHandleTapOptions = {}): Promise { - return this._wrapApiCall('elementHandle.tap', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.tap(options); }); } async selectOption(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise { - return this._wrapApiCall('elementHandle.selectOption', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const result = await channel.selectOption({ ...convertSelectOptionValues(values), ...options }); return result.values; }); } async fill(value: string, options: channels.ElementHandleFillOptions = {}): Promise { - return this._wrapApiCall('elementHandle.fill', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.fill({ value, ...options }); }); } async selectText(options: channels.ElementHandleSelectTextOptions = {}): Promise { - return this._wrapApiCall('elementHandle.selectText', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.selectText(options); }); } async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) { - return this._wrapApiCall('elementHandle.setInputFiles', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.setInputFiles({ files: await convertInputFiles(files), ...options }); }); } async focus(): Promise { - return this._wrapApiCall('elementHandle.focus', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.focus(); }); } async type(text: string, options: channels.ElementHandleTypeOptions = {}): Promise { - return this._wrapApiCall('elementHandle.type', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.type({ text, ...options }); }); } async press(key: string, options: channels.ElementHandlePressOptions = {}): Promise { - return this._wrapApiCall('elementHandle.press', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { await channel.press({ key, ...options }); }); } async check(options: channels.ElementHandleCheckOptions = {}) { - return this._wrapApiCall('elementHandle.check', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.check(options); }); } async uncheck(options: channels.ElementHandleUncheckOptions = {}) { - return this._wrapApiCall('elementHandle.uncheck', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.uncheck(options); }); } async boundingBox(): Promise { - return this._wrapApiCall('elementHandle.boundingBox', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const value = (await channel.boundingBox()).value; return value === undefined ? null : value; }); } async screenshot(options: channels.ElementHandleScreenshotOptions & { path?: string } = {}): Promise { - return this._wrapApiCall('elementHandle.screenshot', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const copy = { ...options }; if (!copy.type) copy.type = determineScreenshotType(options); @@ -240,34 +240,34 @@ export class ElementHandle extends JSHandle implements } async $(selector: string): Promise | null> { - return this._wrapApiCall('elementHandle.$', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return ElementHandle.fromNullable((await channel.querySelector({ selector })).element) as ElementHandle | null; }); } async $$(selector: string): Promise[]> { - return this._wrapApiCall('elementHandle.$$', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const result = await channel.querySelectorAll({ selector }); return result.elements.map(h => ElementHandle.from(h) as ElementHandle); }); } async $eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { - return this._wrapApiCall('elementHandle.$eval', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const result = await channel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async $$eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { - return this._wrapApiCall('elementHandle.$$eval', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const result = await channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async waitForElementState(state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled', options: channels.ElementHandleWaitForElementStateOptions = {}): Promise { - return this._wrapApiCall('elementHandle.waitForElementState', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { return await channel.waitForElementState({ state, ...options }); }); } @@ -275,7 +275,7 @@ export class ElementHandle extends JSHandle implements waitForSelector(selector: string, options: channels.ElementHandleWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise>; waitForSelector(selector: string, options?: channels.ElementHandleWaitForSelectorOptions): Promise | null>; async waitForSelector(selector: string, options: channels.ElementHandleWaitForSelectorOptions = {}): Promise | null> { - return this._wrapApiCall('elementHandle.waitForSelector', async (channel: channels.ElementHandleChannel) => { + return this._wrapApiCall(async (channel: channels.ElementHandleChannel) => { const result = await channel.waitForSelector({ selector, ...options }); return ElementHandle.fromNullable(result.element) as ElementHandle | null; }); diff --git a/src/client/fileChooser.ts b/src/client/fileChooser.ts index 442b271a73..75bcd6122b 100644 --- a/src/client/fileChooser.ts +++ b/src/client/fileChooser.ts @@ -44,7 +44,7 @@ export class FileChooser implements api.FileChooser { } async setFiles(files: string | FilePayload | string[] | FilePayload[], options?: channels.ElementHandleSetInputFilesOptions) { - return this._page._wrapApiCall('fileChooser.setFiles', async () => { + return this._page._wrapApiCall(async () => { return this._elementHandle.setInputFiles(files, options); }); } diff --git a/src/client/frame.ts b/src/client/frame.ts index 69912f3b56..8b4f3ec029 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -30,6 +30,7 @@ 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, @@ -82,23 +83,19 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('goto'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); return network.Response.fromNullable((await channel.goto({ url, ...options, waitUntil })).response); }); } - private _setupNavigationWaiter(name: string, options: { timeout?: number }): Waiter { - const waiter = new Waiter(this, name); + private _setupNavigationWaiter(options: { timeout?: number }, stackTrace: ParsedStackTrace): Waiter { + const waiter = new Waiter(this, '', stackTrace); if (this._page!.isClosed()) 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!')); @@ -110,9 +107,9 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('waitForNavigation'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel, stackTrace: ParsedStackTrace) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); - const waiter = this._setupNavigationWaiter(this._apiName('waitForNavigation'), options); + const waiter = this._setupNavigationWaiter(options, stackTrace); const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`); @@ -148,8 +145,8 @@ export class Frame extends ChannelOwner { - const waiter = this._setupNavigationWaiter(this._apiName('waitForLoadState'), options); + return this._wrapApiCall(async (channel: channels.FrameChannel, stackTrace: ParsedStackTrace) => { + const waiter = this._setupNavigationWaiter(options, stackTrace); await waiter.waitForEvent(this._eventEmitter, 'loadstate', s => { waiter.log(` "${s}" event fired`); return s === state; @@ -165,14 +162,14 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('frameElement'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return ElementHandle.from((await channel.frameElement()).element); }); } async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg): Promise> { assertMaxArguments(arguments.length, 2); - return this._wrapApiCall(this._apiName('evaluateHandle'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const result = await channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return JSHandle.from(result.handle) as any as structs.SmartHandle; }); @@ -180,14 +177,14 @@ export class Frame extends ChannelOwner(pageFunction: structs.PageFunction, arg?: Arg): Promise { assertMaxArguments(arguments.length, 2); - return this._wrapApiCall(this._apiName('evaluate'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const result = await channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async $(selector: string): Promise | null> { - return this._wrapApiCall(this._apiName('$'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const result = await channel.querySelector({ selector }); return ElementHandle.fromNullable(result.element) as ElementHandle | null; }); @@ -196,7 +193,7 @@ export class Frame extends ChannelOwner>; waitForSelector(selector: string, options?: channels.FrameWaitForSelectorOptions): Promise | null>; async waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions = {}): Promise | null> { - return this._wrapApiCall(this._apiName('waitForSelector'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { if ((options as any).visibility) throw new Error('options.visibility is not supported, did you mean options.state?'); if ((options as any).waitFor && (options as any).waitFor !== 'visible') @@ -207,14 +204,14 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('dispatchEvent'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.dispatchEvent({ selector, type, eventInit: serializeArgument(eventInit), ...options }); }); } async $eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { assertMaxArguments(arguments.length, 3); - return this._wrapApiCall(this._apiName('$eval'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const result = await channel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); @@ -222,27 +219,27 @@ export class Frame extends ChannelOwner(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { assertMaxArguments(arguments.length, 3); - return this._wrapApiCall(this._apiName('$$eval'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const result = await channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async $$(selector: string): Promise[]> { - return this._wrapApiCall(this._apiName('$$'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const result = await channel.querySelectorAll({ selector }); return result.elements.map(e => ElementHandle.from(e) as ElementHandle); }); } async content(): Promise { - return this._wrapApiCall(this._apiName('content'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.content()).value; }); } async setContent(html: string, options: channels.FrameSetContentOptions = {}): Promise { - return this._wrapApiCall(this._apiName('setContent'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); await channel.setContent({ html, ...options, waitUntil }); }); @@ -269,7 +266,7 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('addScriptTag'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const copy = { ...options }; if (copy.path) { copy.content = (await fs.promises.readFile(copy.path)).toString(); @@ -280,7 +277,7 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('addStyleTag'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const copy = { ...options }; if (copy.path) { copy.content = (await fs.promises.readFile(copy.path)).toString(); @@ -291,153 +288,153 @@ export class Frame extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return await channel.click({ selector, ...options }); }); } async dblclick(selector: string, options: channels.FrameDblclickOptions = {}) { - return this._wrapApiCall(this._apiName('dblclick'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return await channel.dblclick({ selector, ...options }); }); } async tap(selector: string, options: channels.FrameTapOptions = {}) { - return this._wrapApiCall(this._apiName('tap'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return await channel.tap({ selector, ...options }); }); } async fill(selector: string, value: string, options: channels.FrameFillOptions = {}) { - return this._wrapApiCall(this._apiName('fill'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return await channel.fill({ selector, value, ...options }); }); } async focus(selector: string, options: channels.FrameFocusOptions = {}) { - return this._wrapApiCall(this._apiName('focus'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.focus({ selector, ...options }); }); } async textContent(selector: string, options: channels.FrameTextContentOptions = {}): Promise { - return this._wrapApiCall(this._apiName('textContent'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const value = (await channel.textContent({ selector, ...options })).value; return value === undefined ? null : value; }); } async innerText(selector: string, options: channels.FrameInnerTextOptions = {}): Promise { - return this._wrapApiCall(this._apiName('innerText'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.innerText({ selector, ...options })).value; }); } async innerHTML(selector: string, options: channels.FrameInnerHTMLOptions = {}): Promise { - return this._wrapApiCall(this._apiName('innerHTML'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.innerHTML({ selector, ...options })).value; }); } async getAttribute(selector: string, name: string, options: channels.FrameGetAttributeOptions = {}): Promise { - return this._wrapApiCall(this._apiName('getAttribute'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { const value = (await channel.getAttribute({ selector, name, ...options })).value; return value === undefined ? null : value; }); } async inputValue(selector: string, options: channels.FrameInputValueOptions = {}): Promise { - return this._wrapApiCall(this._apiName('inputValue'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.inputValue({ selector, ...options })).value; }); } async isChecked(selector: string, options: channels.FrameIsCheckedOptions = {}): Promise { - return this._wrapApiCall(this._apiName('isChecked'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.isChecked({ selector, ...options })).value; }); } async isDisabled(selector: string, options: channels.FrameIsDisabledOptions = {}): Promise { - return this._wrapApiCall(this._apiName('isDisabled'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.isDisabled({ selector, ...options })).value; }); } async isEditable(selector: string, options: channels.FrameIsEditableOptions = {}): Promise { - return this._wrapApiCall(this._apiName('isEditable'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.isEditable({ selector, ...options })).value; }); } async isEnabled(selector: string, options: channels.FrameIsEnabledOptions = {}): Promise { - return this._wrapApiCall(this._apiName('isEnabled'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.isEnabled({ selector, ...options })).value; }); } async isHidden(selector: string, options: channels.FrameIsHiddenOptions = {}): Promise { - return this._wrapApiCall(this._apiName('isHidden'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.isHidden({ selector, ...options })).value; }); } async isVisible(selector: string, options: channels.FrameIsVisibleOptions = {}): Promise { - return this._wrapApiCall(this._apiName('isVisible'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.isVisible({ selector, ...options })).value; }); } async hover(selector: string, options: channels.FrameHoverOptions = {}) { - return this._wrapApiCall(this._apiName('hover'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.hover({ selector, ...options }); }); } async selectOption(selector: string, values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options: SelectOptionOptions = {}): Promise { - return this._wrapApiCall(this._apiName('selectOption'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.selectOption({ selector, ...convertSelectOptionValues(values), ...options })).values; }); } async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise { - return this._wrapApiCall(this._apiName('setInputFiles'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.setInputFiles({ selector, files: await convertInputFiles(files), ...options }); }); } async type(selector: string, text: string, options: channels.FrameTypeOptions = {}) { - return this._wrapApiCall(this._apiName('type'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.type({ selector, text, ...options }); }); } async press(selector: string, key: string, options: channels.FramePressOptions = {}) { - return this._wrapApiCall(this._apiName('press'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.press({ selector, key, ...options }); }); } async check(selector: string, options: channels.FrameCheckOptions = {}) { - return this._wrapApiCall(this._apiName('check'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.check({ selector, ...options }); }); } async uncheck(selector: string, options: channels.FrameUncheckOptions = {}) { - return this._wrapApiCall(this._apiName('uncheck'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await channel.uncheck({ selector, ...options }); }); } async waitForTimeout(timeout: number) { - return this._wrapApiCall(this._apiName('waitForTimeout'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { await new Promise(fulfill => setTimeout(fulfill, timeout)); }); } async waitForFunction(pageFunction: structs.PageFunction, arg?: Arg, options: WaitForFunctionOptions = {}): Promise> { - return this._wrapApiCall(this._apiName('waitForFunction'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { if (typeof options.polling === 'string') assert(options.polling === 'raf', 'Unknown polling option: ' + options.polling); const result = await channel.waitForFunction({ @@ -452,7 +449,7 @@ export class Frame extends ChannelOwner { - return this._wrapApiCall(this._apiName('title'), async (channel: channels.FrameChannel) => { + return this._wrapApiCall(async (channel: channels.FrameChannel) => { return (await channel.title()).value; }); } diff --git a/src/client/jsHandle.ts b/src/client/jsHandle.ts index 6963d5e244..c6421ad1c4 100644 --- a/src/client/jsHandle.ts +++ b/src/client/jsHandle.ts @@ -34,28 +34,28 @@ export class JSHandle extends ChannelOwner(pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { - return this._wrapApiCall('jsHandle.evaluate', async (channel: channels.JSHandleChannel) => { + return this._wrapApiCall(async (channel: channels.JSHandleChannel) => { const result = await channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); } async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg): Promise> { - return this._wrapApiCall('jsHandle.evaluateHandle', async (channel: channels.JSHandleChannel) => { + return this._wrapApiCall(async (channel: channels.JSHandleChannel) => { const result = await channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return JSHandle.from(result.handle) as any as structs.SmartHandle; }); } async getProperty(propertyName: string): Promise { - return this._wrapApiCall('jsHandle.getProperty', async (channel: channels.JSHandleChannel) => { + return this._wrapApiCall(async (channel: channels.JSHandleChannel) => { const result = await channel.getProperty({ name: propertyName }); return JSHandle.from(result.handle); }); } async getProperties(): Promise> { - return this._wrapApiCall('jsHandle.getProperties', async (channel: channels.JSHandleChannel) => { + return this._wrapApiCall(async (channel: channels.JSHandleChannel) => { const map = new Map(); for (const { name, value } of (await channel.getPropertyList()).properties) map.set(name, JSHandle.from(value)); @@ -64,7 +64,7 @@ export class JSHandle extends ChannelOwner { - return this._wrapApiCall('jsHandle.jsonValue', async (channel: channels.JSHandleChannel) => { + return this._wrapApiCall(async (channel: channels.JSHandleChannel) => { return parseResult((await channel.jsonValue()).value); }); } @@ -74,7 +74,7 @@ export class JSHandle extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.JSHandleChannel) => { return await channel.dispose(); }); } diff --git a/src/client/network.ts b/src/client/network.ts index 81dbf742b8..c5c43db6a3 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -26,6 +26,7 @@ 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, @@ -132,7 +133,7 @@ export class Request extends ChannelOwner { - return this._wrapApiCall('request.response', async (channel: channels.RequestChannel) => { + return this._wrapApiCall(async (channel: channels.RequestChannel) => { return Response.fromNullable((await channel.response()).response); }); } @@ -258,13 +259,13 @@ export class Route extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.RouteChannel) => { await channel.abort({ errorCode }); }); } async fulfill(options: { status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) { - return this._wrapApiCall('route.fulfill', async (channel: channels.RouteChannel) => { + return this._wrapApiCall(async (channel: channels.RouteChannel) => { let body = ''; let isBase64 = false; let length = 0; @@ -303,17 +304,17 @@ export class Route extends ChannelOwner { - return await this._continue('route.intercept', options, true); + return await this._continue(options, true); } async continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) { - await this._continue('route.continue', options, false); + await this._continue(options, false); } - async _continue(apiName: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: NotInterceptResponse): Promise; - async _continue(apiName: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse): Promise; - async _continue(apiName: string, options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean): Promise { - return await this._wrapApiCall(apiName, async (channel: channels.RouteChannel) => { + async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: NotInterceptResponse): Promise; + async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: InterceptResponse): Promise; + async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, interceptResponse: boolean): Promise { + return await this._wrapApiCall(async (channel: channels.RouteChannel) => { const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData; const result = await channel.continue({ url: options.url, @@ -329,7 +330,7 @@ export class Route extends ChannelOwner { - return this._wrapApiCall('response.body', async (channel: channels.RouteChannel) => { + return this._wrapApiCall(async (channel: channels.RouteChannel) => { return Buffer.from((await channel.responseBody()).binary, 'base64'); }); } @@ -390,14 +391,16 @@ export class Response extends ChannelOwner { - const result = await this._channel.finished(); - if (result.error) - return new Error(result.error); - return null; + return this._wrapApiCall(async (channel: channels.ResponseChannel) => { + const result = await channel.finished(); + if (result.error) + return new Error(result.error); + return null; + }); } async body(): Promise { - return this._wrapApiCall('response.body', async (channel: channels.ResponseChannel) => { + return this._wrapApiCall(async (channel: channels.ResponseChannel) => { return Buffer.from((await channel.body()).binary, 'base64'); }); } @@ -421,11 +424,15 @@ export class Response extends ChannelOwner { - return (await this._channel.serverAddr()).value || null; + return this._wrapApiCall(async (channel: channels.ResponseChannel) => { + return (await channel.serverAddr()).value || null; + }); } async securityDetails(): Promise { - return (await this._channel.securityDetails()).value || null; + return this._wrapApiCall(async (channel: channels.ResponseChannel) => { + return (await channel.securityDetails()).value || null; + }); } } @@ -469,18 +476,20 @@ 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, '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')); - if (event !== Events.WebSocket.Close) - waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed')); - waiter.rejectOnEvent(this._page, Events.Page.Close, new Error('Page closed')); - const result = await waiter.waitForEvent(this, event, predicate as any); - waiter.dispose(); - return result; + return this._wrapApiCall(async (channel: channels.WebSocketChannel, stackTrace: ParsedStackTrace) => { + 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); + waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); + if (event !== Events.WebSocket.Error) + waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error')); + if (event !== Events.WebSocket.Close) + waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed')); + waiter.rejectOnEvent(this._page, Events.Page.Close, new Error('Page closed')); + const result = await waiter.waitForEvent(this, event, predicate as any); + waiter.dispose(); + return result; + }); } } diff --git a/src/client/page.ts b/src/client/page.ts index e8304a74e0..91b09c264c 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -46,6 +46,7 @@ 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, @@ -80,7 +81,6 @@ export class Page extends ChannelOwner any>(); readonly _timeoutSettings: TimeoutSettings; - _isPageCall = false; private _video: Video | null = null; readonly _opener: Page | null; @@ -121,7 +121,6 @@ export class Page extends ChannelOwner { const artifactObject = Artifact.from(artifact); artifactObject._isRemote = !!this._browserContext._browser && !!this._browserContext._browser._remoteType; - artifactObject._apiName = 'download'; this.emit(Events.Page.Download, new Download(this, url, suggestedFilename, artifactObject)); }); this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple))); @@ -132,7 +131,6 @@ export class Page extends ChannelOwner this._onRoute(Route.from(route), Request.from(request))); this._channel.on('video', ({ artifact }) => { const artifactObject = Artifact.from(artifact); - artifactObject._apiName = 'video'; this._forceVideo()._artifactReady(artifactObject); }); this._channel.on('webSocket', ({ webSocket }) => this.emit(Events.Page.WebSocket, WebSocket.from(webSocket))); @@ -252,58 +250,49 @@ export class Page extends ChannelOwner(func: () => T): T { - try { - this._isPageCall = true; - return func(); - } finally { - this._isPageCall = false; - } - } - async $(selector: string): Promise | null> { - return this._attributeToPage(() => this._mainFrame.$(selector)); + return this._mainFrame.$(selector); } waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise>; waitForSelector(selector: string, options?: channels.FrameWaitForSelectorOptions): Promise | null>; async waitForSelector(selector: string, options?: channels.FrameWaitForSelectorOptions): Promise | null> { - return this._attributeToPage(() => this._mainFrame.waitForSelector(selector, options)); + return this._mainFrame.waitForSelector(selector, options); } async dispatchEvent(selector: string, type: string, eventInit?: any, options?: channels.FrameDispatchEventOptions): Promise { - return this._attributeToPage(() => this._mainFrame.dispatchEvent(selector, type, eventInit, options)); + return this._mainFrame.dispatchEvent(selector, type, eventInit, options); } async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg): Promise> { assertMaxArguments(arguments.length, 2); - return this._attributeToPage(() => this._mainFrame.evaluateHandle(pageFunction, arg)); + return this._mainFrame.evaluateHandle(pageFunction, arg); } async $eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { assertMaxArguments(arguments.length, 3); - return this._attributeToPage(() => this._mainFrame.$eval(selector, pageFunction, arg)); + return this._mainFrame.$eval(selector, pageFunction, arg); } async $$eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { assertMaxArguments(arguments.length, 3); - return this._attributeToPage(() => this._mainFrame.$$eval(selector, pageFunction, arg)); + return this._mainFrame.$$eval(selector, pageFunction, arg); } async $$(selector: string): Promise[]> { - return this._attributeToPage(() => this._mainFrame.$$(selector)); + return this._mainFrame.$$(selector); } async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; } = {}): Promise { - return this._attributeToPage(() => this._mainFrame.addScriptTag(options)); + return this._mainFrame.addScriptTag(options); } async addStyleTag(options: { url?: string; path?: string; content?: string; } = {}): Promise { - return this._attributeToPage(() => this._mainFrame.addStyleTag(options)); + return this._mainFrame.addStyleTag(options); } async exposeFunction(name: string, callback: Function) { - return this._wrapApiCall('page.exposeFunction', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { await channel.exposeBinding({ name }); const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args); this._bindings.set(name, binding); @@ -311,56 +300,56 @@ export class Page extends ChannelOwner any, options: { handle?: boolean } = {}) { - return this._wrapApiCall('page.exposeBinding', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { await channel.exposeBinding({ name, needsHandle: options.handle }); this._bindings.set(name, callback); }); } async setExtraHTTPHeaders(headers: Headers) { - return this._wrapApiCall('page.setExtraHTTPHeaders', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { validateHeaders(headers); await channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); }); } url(): string { - return this._attributeToPage(() => this._mainFrame.url()); + return this._mainFrame.url(); } async content(): Promise { - return this._attributeToPage(() => this._mainFrame.content()); + return this._mainFrame.content(); } async setContent(html: string, options?: channels.FrameSetContentOptions): Promise { - return this._attributeToPage(() => this._mainFrame.setContent(html, options)); + return this._mainFrame.setContent(html, options); } async goto(url: string, options?: channels.FrameGotoOptions): Promise { - return this._attributeToPage(() => this._mainFrame.goto(url, options)); + return this._mainFrame.goto(url, options); } async reload(options: channels.PageReloadOptions = {}): Promise { - return this._wrapApiCall('page.reload', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); return Response.fromNullable((await channel.reload({ ...options, waitUntil })).response); }); } async waitForLoadState(state?: LifecycleEvent, options?: { timeout?: number }): Promise { - return this._attributeToPage(() => this._mainFrame.waitForLoadState(state, options)); + return this._mainFrame.waitForLoadState(state, options); } async waitForNavigation(options?: WaitForNavigationOptions): Promise { - return this._attributeToPage(() => this._mainFrame.waitForNavigation(options)); + return this._mainFrame.waitForNavigation(options); } async waitForURL(url: URLMatch, options?: { waitUntil?: LifecycleEvent, timeout?: number }): Promise { - return this._attributeToPage(() => this._mainFrame.waitForURL(url, options)); + return this._mainFrame.waitForURL(url, options); } async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise), options: { timeout?: number } = {}): Promise { - return this._wrapApiCall('page.waitForRequest', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { const predicate = (request: Request) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) return urlMatches(request.url(), urlOrPredicate); @@ -368,12 +357,12 @@ export class Page extends ChannelOwner boolean | Promise), options: { timeout?: number } = {}): Promise { - return this._wrapApiCall('page.waitForResponse', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { const predicate = (response: Response) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) return urlMatches(response.url(), urlOrPredicate); @@ -381,20 +370,20 @@ export class Page extends ChannelOwner { - return this._wrapApiCall('page.waitForEvent', async (channel: channels.PageChannel) => { - return this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`); + return this._wrapApiCall(async (channel: channels.PageChannel, stackTrace: ParsedStackTrace) => { + return this._waitForEvent(event, optionsOrPredicate, stackTrace, `waiting for event "${event}"`); }); } - private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise { + private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, stackTrace: ParsedStackTrace, 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, 'page', event); + const waiter = Waiter.createForEvent(this, event, stackTrace); if (logLine) waiter.log(logLine); waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`); @@ -408,21 +397,21 @@ export class Page extends ChannelOwner { - return this._wrapApiCall('page.goBack', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); return Response.fromNullable((await channel.goBack({ ...options, waitUntil })).response); }); } async goForward(options: channels.PageGoForwardOptions = {}): Promise { - return this._wrapApiCall('page.goForward', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); return Response.fromNullable((await channel.goForward({ ...options, waitUntil })).response); }); } async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null } = {}) { - return this._wrapApiCall('page.emulateMedia', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { await channel.emulateMedia({ media: options.media === null ? 'null' : options.media, colorScheme: options.colorScheme === null ? 'null' : options.colorScheme, @@ -432,7 +421,7 @@ export class Page extends ChannelOwner { + return this._wrapApiCall(async (channel: channels.PageChannel) => { this._viewportSize = viewportSize; await channel.setViewportSize({ viewportSize }); }); @@ -444,18 +433,18 @@ export class Page extends ChannelOwner(pageFunction: structs.PageFunction, arg?: Arg): Promise { assertMaxArguments(arguments.length, 2); - return this._attributeToPage(() => this._mainFrame.evaluate(pageFunction, arg)); + return this._mainFrame.evaluate(pageFunction, arg); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - return this._wrapApiCall('page.addInitScript', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const source = await evaluationScript(script, arg); await channel.addInitScript({ source }); }); } async route(url: URLMatch, handler: RouteHandler): Promise { - return this._wrapApiCall('page.route', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { this._routes.push({ url, handler }); if (this._routes.length === 1) await channel.setNetworkInterceptionEnabled({ enabled: true }); @@ -463,7 +452,7 @@ export class Page extends ChannelOwner { - return this._wrapApiCall('page.unroute', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); if (this._routes.length === 0) await channel.setNetworkInterceptionEnabled({ enabled: false }); @@ -471,7 +460,7 @@ export class Page extends ChannelOwner { - return this._wrapApiCall('page.screenshot', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const copy = { ...options }; if (!copy.type) copy.type = determineScreenshotType(options); @@ -486,18 +475,18 @@ export class Page extends ChannelOwner { - return this._attributeToPage(() => this._mainFrame.title()); + return this._mainFrame.title(); } async bringToFront(): Promise { - return this._wrapApiCall('page.bringToFront', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { await channel.bringToFront(); }); } async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) { try { - await this._wrapApiCall('page.close', async (channel: channels.PageChannel) => { + await this._wrapApiCall(async (channel: channels.PageChannel) => { await channel.close(options); if (this._ownedContext) await this._ownedContext.close(); @@ -514,103 +503,103 @@ export class Page extends ChannelOwner this._mainFrame.click(selector, options)); + return this._mainFrame.click(selector, options); } async dblclick(selector: string, options?: channels.FrameDblclickOptions) { - return this._attributeToPage(() => this._mainFrame.dblclick(selector, options)); + return this._mainFrame.dblclick(selector, options); } async tap(selector: string, options?: channels.FrameTapOptions) { - return this._attributeToPage(() => this._mainFrame.tap(selector, options)); + return this._mainFrame.tap(selector, options); } async fill(selector: string, value: string, options?: channels.FrameFillOptions) { - return this._attributeToPage(() => this._mainFrame.fill(selector, value, options)); + return this._mainFrame.fill(selector, value, options); } async focus(selector: string, options?: channels.FrameFocusOptions) { - return this._attributeToPage(() => this._mainFrame.focus(selector, options)); + return this._mainFrame.focus(selector, options); } async textContent(selector: string, options?: channels.FrameTextContentOptions): Promise { - return this._attributeToPage(() => this._mainFrame.textContent(selector, options)); + return this._mainFrame.textContent(selector, options); } async innerText(selector: string, options?: channels.FrameInnerTextOptions): Promise { - return this._attributeToPage(() => this._mainFrame.innerText(selector, options)); + return this._mainFrame.innerText(selector, options); } async innerHTML(selector: string, options?: channels.FrameInnerHTMLOptions): Promise { - return this._attributeToPage(() => this._mainFrame.innerHTML(selector, options)); + return this._mainFrame.innerHTML(selector, options); } async getAttribute(selector: string, name: string, options?: channels.FrameGetAttributeOptions): Promise { - return this._attributeToPage(() => this._mainFrame.getAttribute(selector, name, options)); + return this._mainFrame.getAttribute(selector, name, options); } async inputValue(selector: string, options?: channels.FrameInputValueOptions): Promise { - return this._attributeToPage(() => this._mainFrame.inputValue(selector, options)); + return this._mainFrame.inputValue(selector, options); } async isChecked(selector: string, options?: channels.FrameIsCheckedOptions): Promise { - return this._attributeToPage(() => this._mainFrame.isChecked(selector, options)); + return this._mainFrame.isChecked(selector, options); } async isDisabled(selector: string, options?: channels.FrameIsDisabledOptions): Promise { - return this._attributeToPage(() => this._mainFrame.isDisabled(selector, options)); + return this._mainFrame.isDisabled(selector, options); } async isEditable(selector: string, options?: channels.FrameIsEditableOptions): Promise { - return this._attributeToPage(() => this._mainFrame.isEditable(selector, options)); + return this._mainFrame.isEditable(selector, options); } async isEnabled(selector: string, options?: channels.FrameIsEnabledOptions): Promise { - return this._attributeToPage(() => this._mainFrame.isEnabled(selector, options)); + return this._mainFrame.isEnabled(selector, options); } async isHidden(selector: string, options?: channels.FrameIsHiddenOptions): Promise { - return this._attributeToPage(() => this._mainFrame.isHidden(selector, options)); + return this._mainFrame.isHidden(selector, options); } async isVisible(selector: string, options?: channels.FrameIsVisibleOptions): Promise { - return this._attributeToPage(() => this._mainFrame.isVisible(selector, options)); + return this._mainFrame.isVisible(selector, options); } async hover(selector: string, options?: channels.FrameHoverOptions) { - return this._attributeToPage(() => this._mainFrame.hover(selector, options)); + return this._mainFrame.hover(selector, options); } async selectOption(selector: string, values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null, options?: SelectOptionOptions): Promise { - return this._attributeToPage(() => this._mainFrame.selectOption(selector, values, options)); + return this._mainFrame.selectOption(selector, values, options); } async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options?: channels.FrameSetInputFilesOptions): Promise { - return this._attributeToPage(() => this._mainFrame.setInputFiles(selector, files, options)); + return this._mainFrame.setInputFiles(selector, files, options); } async type(selector: string, text: string, options?: channels.FrameTypeOptions) { - return this._attributeToPage(() => this._mainFrame.type(selector, text, options)); + return this._mainFrame.type(selector, text, options); } async press(selector: string, key: string, options?: channels.FramePressOptions) { - return this._attributeToPage(() => this._mainFrame.press(selector, key, options)); + return this._mainFrame.press(selector, key, options); } async check(selector: string, options?: channels.FrameCheckOptions) { - return this._attributeToPage(() => this._mainFrame.check(selector, options)); + return this._mainFrame.check(selector, options); } async uncheck(selector: string, options?: channels.FrameUncheckOptions) { - return this._attributeToPage(() => this._mainFrame.uncheck(selector, options)); + return this._mainFrame.uncheck(selector, options); } async waitForTimeout(timeout: number) { - return this._attributeToPage(() => this._mainFrame.waitForTimeout(timeout)); + return this._mainFrame.waitForTimeout(timeout); } async waitForFunction(pageFunction: structs.PageFunction, arg?: Arg, options?: WaitForFunctionOptions): Promise> { - return this._attributeToPage(() => this._mainFrame.waitForFunction(pageFunction, arg, options)); + return this._mainFrame.waitForFunction(pageFunction, arg, options); } workers(): Worker[] { @@ -646,13 +635,13 @@ export class Page extends ChannelOwner { + return this.context()._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.pause(); }); } async pdf(options: PDFOptions = {}): Promise { - return this._wrapApiCall('page.pdf', async (channel: channels.PageChannel) => { + return this._wrapApiCall(async (channel: channels.PageChannel) => { const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams; if (transportOptions.margin) transportOptions.margin = { ...transportOptions.margin }; diff --git a/src/client/tracing.ts b/src/client/tracing.ts index 12c0372415..e0e44a35c1 100644 --- a/src/client/tracing.ts +++ b/src/client/tracing.ts @@ -27,18 +27,17 @@ export class Tracing implements api.Tracing { } async start(options: { name?: string, snapshots?: boolean, screenshots?: boolean } = {}) { - await this._context._wrapApiCall('tracing.start', async (channel: channels.BrowserContextChannel) => { + await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => { return await channel.tracingStart(options); }); } async stop(options: { path?: string } = {}) { - await this._context._wrapApiCall('tracing.stop', async (channel: channels.BrowserContextChannel) => { + await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => { await channel.tracingStop(); if (options.path) { const result = await channel.tracingExport(); const artifact = Artifact.from(result.artifact); - artifact._apiName = 'tracing'; if (this._context.browser()?._remoteType) artifact._isRemote = true; await artifact.saveAs(options.path); diff --git a/src/client/waiter.ts b/src/client/waiter.ts index 72588120d3..9adbd2cfd9 100644 --- a/src/client/waiter.ts +++ b/src/client/waiter.ts @@ -15,7 +15,7 @@ */ import { EventEmitter } from 'events'; -import { rewriteErrorMessage } from '../utils/stackTrace'; +import { ParsedStackTrace, rewriteErrorMessage } from '../utils/stackTrace'; import { TimeoutError } from '../utils/errors'; import { createGuid } from '../utils/utils'; import { ChannelOwner } from './channelOwner'; @@ -30,17 +30,17 @@ export class Waiter { private _waitId: string; private _error: string | undefined; - constructor(channelOwner: ChannelOwner, apiName: string) { + constructor(channelOwner: ChannelOwner, event: string, stackTrace: ParsedStackTrace) { this._waitId = createGuid(); this._channelOwner = channelOwner; - this._channelOwner._waitForEventInfoBefore(this._waitId, apiName); + this._channelOwner._waitForEventInfoBefore(this._waitId, event, stackTrace); this._dispose = [ () => this._channelOwner._waitForEventInfoAfter(this._waitId, this._error) ]; } - static createForEvent(channelOwner: ChannelOwner, target: string, event: string) { - return new Waiter(channelOwner, `${target}.waitForEvent(${event})`); + static createForEvent(channelOwner: ChannelOwner, event: string, stackTrace: ParsedStackTrace) { + return new Waiter(channelOwner, event, stackTrace); } async waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise): Promise { @@ -82,7 +82,7 @@ export class Waiter { dispose(); this._error = e.message; this.dispose(); - rewriteErrorMessage(e, e.message + formatLogRecording(this._logs) + kLoggingNote); + rewriteErrorMessage(e, e.message + formatLogRecording(this._logs)); throw e; } } @@ -126,8 +126,6 @@ function waitForTimeout(timeout: number): { promise: Promise, dispose: () return { promise, dispose }; } -const kLoggingNote = `\nNote: use DEBUG=pw:api environment variable to capture Playwright logs.`; - function formatLogRecording(log: string[]): string { if (!log.length) return ''; diff --git a/src/client/worker.ts b/src/client/worker.ts index 1c4076cfb2..f8692b69c6 100644 --- a/src/client/worker.ts +++ b/src/client/worker.ts @@ -48,7 +48,7 @@ export class Worker extends ChannelOwner(pageFunction: structs.PageFunction, arg?: Arg): Promise { assertMaxArguments(arguments.length, 2); - return this._wrapApiCall('worker.evaluate', async (channel: channels.WorkerChannel) => { + return this._wrapApiCall(async (channel: channels.WorkerChannel) => { const result = await channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return parseResult(result.value); }); @@ -56,7 +56,7 @@ export class Worker extends ChannelOwner(pageFunction: structs.PageFunction, arg?: Arg): Promise> { assertMaxArguments(arguments.length, 2); - return this._wrapApiCall('worker.evaluateHandle', async (channel: channels.WorkerChannel) => { + return this._wrapApiCall(async (channel: channels.WorkerChannel) => { const result = await channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }); return JSHandle.from(result.handle) as any as structs.SmartHandle; }); diff --git a/src/dispatchers/dispatcher.ts b/src/dispatchers/dispatcher.ts index f42f4b1d2a..055a6096d9 100644 --- a/src/dispatchers/dispatcher.ts +++ b/src/dispatchers/dispatcher.ts @@ -237,7 +237,6 @@ export class DispatcherConnection { const info = params.info; switch (info.phase) { case 'before': { - callMetadata.apiName = info.apiName; this._waitOperations.set(info.waitId, callMetadata); await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata); return; @@ -267,7 +266,7 @@ export class DispatcherConnection { // Dispatching error callMetadata.error = e.message; if (callMetadata.log.length) - rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote); + rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log)); error = serializeError(e); } finally { callMetadata.endTime = monotonicTime(); @@ -297,8 +296,6 @@ export class DispatcherConnection { } } -const kLoggingNote = `\nNote: use DEBUG=pw:api environment variable to capture Playwright logs.`; - function formatLogRecording(log: string[]): string { if (!log.length) return ''; diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index ce9a11426e..16e6be9011 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -38,7 +38,7 @@ export type Metadata = { export type WaitForEventInfo = { waitId: string, phase: 'before' | 'after' | 'log', - apiName?: string, + event?: string, message?: string, error?: string, }; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 4ec0ef4aae..28a142db68 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -41,7 +41,7 @@ WaitForEventInfo: - before - after - log - apiName: string? + event: string? message: string? error: string? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index b1eaeb7121..8c00d94a10 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -46,7 +46,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.WaitForEventInfo = tObject({ waitId: tString, phase: tEnum(['before', 'after', 'log']), - apiName: tOptional(tString), + event: tOptional(tString), message: tOptional(tString), error: tOptional(tString), }); diff --git a/src/server/supplements/recorder/recorderUtils.ts b/src/server/supplements/recorder/recorderUtils.ts index b0336245b5..aeed16d05c 100644 --- a/src/server/supplements/recorder/recorderUtils.ts +++ b/src/server/supplements/recorder/recorderUtils.ts @@ -18,7 +18,9 @@ import { CallMetadata } from '../../instrumentation'; import { CallLog, CallLogStatus } from './recorderTypes'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { - const title = metadata.apiName || metadata.method; + let title = metadata.apiName || metadata.method; + if (metadata.method === 'waitForEventInfo') + title += `(${metadata.params.info.event})`; if (metadata.error) status = 'error'; const params = { diff --git a/src/utils/stackTrace.ts b/src/utils/stackTrace.ts index faea322f41..0310b37ea1 100644 --- a/src/utils/stackTrace.ts +++ b/src/utils/stackTrace.ts @@ -37,28 +37,44 @@ const PW_LIB_DIRS = [ 'playwright-firefox', 'playwright-webkit', path.join('@playwright', 'test'), -].map(packageName => path.sep + path.join(packageName, 'lib')); +].map(packageName => path.sep + packageName); -export function captureStackTrace(): { stack: string, frames: StackFrame[] } { +const runnerLib = path.join('@playwright', 'test', 'lib', 'test'); +const runnerSrc = path.join('src', 'test'); + +export type ParsedStackTrace = { + frames: StackFrame[]; + frameTexts: string[]; + apiName: string; +}; + +export function captureStackTrace(): ParsedStackTrace { const stackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 30; - const stack = new Error().stack!; + const error = new Error(); + const stack = error.stack!; Error.stackTraceLimit = stackTraceLimit; const frames: StackFrame[] = []; - for (const line of stack.split('\n')) { + const frameTexts: string[] = []; + const lines = stack.split('\n').reverse(); + let apiName = ''; + + const isTesting = process.env.PWTEST_CLI_ALLOW_TEST_COMMAND || isUnderTest(); + + for (const line of lines) { const frame = stackUtils.parseLine(line); if (!frame || !frame.file) continue; if (frame.file.startsWith('internal')) continue; const fileName = path.resolve(process.cwd(), frame.file); - if (PW_LIB_DIRS.some(libDir => fileName.includes(libDir))) - continue; - const isTesting = process.env.PWTEST_CLI_ALLOW_TEST_COMMAND || isUnderTest(); - if (isTesting && fileName.includes(path.join('playwright', 'src'))) - continue; if (isTesting && fileName.includes(path.join('playwright', 'tests', 'config', 'coverage.js'))) continue; + if (!fileName.includes(runnerLib) && !(isTesting && fileName.includes(runnerSrc)) && PW_LIB_DIRS.map(p => path.join(p, isTesting ? 'src' : 'lib')).some(libDir => fileName.includes(libDir))) { + apiName = frame.function ? frame.function[0].toLowerCase() + frame.function.slice(1) : ''; + break; + } + frameTexts.push(line); frames.push({ file: fileName, line: frame.line, @@ -66,7 +82,9 @@ export function captureStackTrace(): { stack: string, frames: StackFrame[] } { function: frame.function, }); } - return { stack, frames }; + frames.reverse(); + frameTexts.reverse(); + return { frames, frameTexts, apiName }; } export function splitErrorMessage(message: string): { name: string, message: string } { diff --git a/tests/page/page-wait-for-load-state.spec.ts b/tests/page/page-wait-for-load-state.spec.ts index 73be5e99ef..4440673bad 100644 --- a/tests/page/page-wait-for-load-state.spec.ts +++ b/tests/page/page-wait-for-load-state.spec.ts @@ -36,6 +36,7 @@ it('should respect timeout', async ({page, server}) => { await page.goto(server.PREFIX + '/one-style.html', {waitUntil: 'domcontentloaded'}); const error = await page.waitForLoadState('load', { timeout: 1 }).catch(e => e); expect(error.message).toContain('page.waitForLoadState: Timeout 1ms exceeded.'); + expect(error.stack.split('\n')[1]).toContain(__filename); }); it('should resolve immediately if loaded', async ({page, server}) => { diff --git a/tests/page/page-wait-for-url.spec.ts b/tests/page/page-wait-for-url.spec.ts index fa9305a6e8..1faab936f0 100644 --- a/tests/page/page-wait-for-url.spec.ts +++ b/tests/page/page-wait-for-url.spec.ts @@ -27,7 +27,7 @@ it('should respect timeout', async ({page, server}) => { const promise = page.waitForURL('**/frame.html', { timeout: 2500 }).catch(e => e); await page.goto(server.EMPTY_PAGE); const error = await promise; - expect(error.message).toContain('page.waitForNavigation: Timeout 2500ms exceeded.'); + expect(error.message).toContain('page.waitForURL: Timeout 2500ms exceeded.'); }); it('should work with both domcontentloaded and load', async ({page, server}) => {