From 056f0e290d7de20dda36fa0596e8bb7e96fd694b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 16 Jul 2020 14:32:21 -0700 Subject: [PATCH] feat(rpc): ensure that error stack traces point to the user code (#2961) This also adds more "_wrapApiCall" calls for correct logs and stack traces. --- src/logger.ts | 2 +- src/rpc/client/browser.ts | 30 ++++---- src/rpc/client/browserContext.ts | 96 ++++++++++++++++-------- src/rpc/client/browserServer.ts | 8 +- src/rpc/client/cdpSession.ts | 10 ++- src/rpc/client/channelOwner.ts | 27 +++++-- src/rpc/client/chromiumBrowser.ts | 12 ++- src/rpc/client/chromiumBrowserContext.ts | 6 +- src/rpc/client/dialog.ts | 8 +- src/rpc/client/electron.ts | 11 ++- src/rpc/client/page.ts | 72 ++++++++++++------ test/browsercontext.spec.js | 10 +-- test/chromium/session.spec.js | 2 +- test/cookies.spec.js | 2 +- test/emulation.spec.js | 6 +- test/evaluation.jest.js | 2 +- test/network.spec.js | 2 +- test/page.spec.js | 2 +- test/permissions.spec.js | 2 +- test/waittask.spec.js | 2 +- 20 files changed, 201 insertions(+), 111 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index a1cdd7f8ef..5e4ec35c45 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -140,7 +140,7 @@ const colorMap = new Map([ ['reset', 0], ]); -class DebugLoggerSink { +export class DebugLoggerSink { private _debuggers = new Map(); isEnabled(name: string, severity: LoggerSeverity): boolean { diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts index 0c41df7f95..6b93b6bb4f 100644 --- a/src/rpc/client/browser.ts +++ b/src/rpc/client/browser.ts @@ -54,14 +54,16 @@ export class Browser extends ChannelOwner { async newContext(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise { const logger = options.logger; options = { ...options, logger: undefined }; - const contextOptions: BrowserContextOptions = { - ...options, - extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, - }; - const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context); - this._contexts.add(context); - context._logger = logger || this._logger; - return context; + return this._wrapApiCall('browser.newContext', async () => { + const contextOptions: BrowserContextOptions = { + ...options, + extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, + }; + const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context); + this._contexts.add(context); + context._logger = logger || this._logger; + return context; + }); } contexts(): BrowserContext[] { @@ -81,10 +83,12 @@ export class Browser extends ChannelOwner { } async close(): Promise { - if (!this._isClosedOrClosing) { - this._isClosedOrClosing = true; - await this._channel.close(); - } - await this._closedPromise; + return this._wrapApiCall('browser.close', async () => { + if (!this._isClosedOrClosing) { + this._isClosedOrClosing = true; + await this._channel.close(); + } + await this._closedPromise; + }); } } diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index 43d02a09a0..80d1336252 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -98,9 +98,11 @@ export class BrowserContext extends ChannelOwner { - if (this._ownerPage) - throw new Error('Please use browser.newContext()'); - return Page.from((await this._channel.newPage()).page); + return this._wrapApiCall('browserContext.newPage', async () => { + if (this._ownerPage) + throw new Error('Please use browser.newContext()'); + return Page.from((await this._channel.newPage()).page); + }); } async cookies(urls?: string | string[]): Promise { @@ -108,55 +110,77 @@ export class BrowserContext extends ChannelOwner { + return (await this._channel.cookies({ urls: urls as string[] })).cookies; + }); } async addCookies(cookies: network.SetNetworkCookieParam[]): Promise { - await this._channel.addCookies({ cookies }); + return this._wrapApiCall('browserContext.addCookies', async () => { + await this._channel.addCookies({ cookies }); + }); } async clearCookies(): Promise { - await this._channel.clearCookies(); + return this._wrapApiCall('browserContext.clearCookies', async () => { + await this._channel.clearCookies(); + }); } async grantPermissions(permissions: string[], options?: { origin?: string }): Promise { - await this._channel.grantPermissions({ permissions, ...options }); + return this._wrapApiCall('browserContext.grantPermissions', async () => { + await this._channel.grantPermissions({ permissions, ...options }); + }); } async clearPermissions(): Promise { - await this._channel.clearPermissions(); + return this._wrapApiCall('browserContext.clearPermissions', async () => { + await this._channel.clearPermissions(); + }); } async setGeolocation(geolocation: types.Geolocation | null): Promise { - await this._channel.setGeolocation({ geolocation }); + return this._wrapApiCall('browserContext.setGeolocation', async () => { + await this._channel.setGeolocation({ geolocation }); + }); } async setExtraHTTPHeaders(headers: types.Headers): Promise { - await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); + return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async () => { + await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); + }); } async setOffline(offline: boolean): Promise { - await this._channel.setOffline({ offline }); + return this._wrapApiCall('browserContext.setOffline', async () => { + await this._channel.setOffline({ offline }); + }); } async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise { - await this._channel.setHTTPCredentials({ httpCredentials }); + return this._wrapApiCall('browserContext.setHTTPCredentials', async () => { + await this._channel.setHTTPCredentials({ httpCredentials }); + }); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { - const source = await helper.evaluationScript(script, arg); - await this._channel.addInitScript({ source }); + return this._wrapApiCall('browserContext.addInitScript', async () => { + const source = await helper.evaluationScript(script, arg); + await this._channel.addInitScript({ source }); + }); } async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise { - for (const page of this.pages()) { - if (page._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered in one of the pages`); - } - if (this._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered`); - this._bindings.set(name, binding); - await this._channel.exposeBinding({ name }); + return this._wrapApiCall('browserContext.exposeBinding', async () => { + for (const page of this.pages()) { + if (page._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered in one of the pages`); + } + if (this._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + this._bindings.set(name, binding); + await this._channel.exposeBinding({ name }); + }); } async exposeFunction(name: string, playwrightFunction: Function): Promise { @@ -164,15 +188,19 @@ export class BrowserContext extends ChannelOwner { - this._routes.push({ url, handler }); - if (this._routes.length === 1) - await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + return this._wrapApiCall('browserContext.route', async () => { + this._routes.push({ url, handler }); + if (this._routes.length === 1) + await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + }); } async unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise { - this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); - if (this._routes.length === 0) - await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + return this._wrapApiCall('browserContext.unroute', async () => { + this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); + if (this._routes.length === 0) + await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + }); } async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { @@ -196,10 +224,12 @@ export class BrowserContext extends ChannelOwner { - if (!this._isClosedOrClosing) { - this._isClosedOrClosing = true; - await this._channel.close(); - } - await this._closedPromise; + return this._wrapApiCall('browserContext.close', async () => { + if (!this._isClosedOrClosing) { + this._isClosedOrClosing = true; + await this._channel.close(); + } + await this._closedPromise; + }); } } diff --git a/src/rpc/client/browserServer.ts b/src/rpc/client/browserServer.ts index 39bbb2bab5..d92899ee5d 100644 --- a/src/rpc/client/browserServer.ts +++ b/src/rpc/client/browserServer.ts @@ -38,11 +38,15 @@ export class BrowserServer extends ChannelOwner { - await this._channel.kill(); + return this._wrapApiCall('browserServer.kill', async () => { + await this._channel.kill(); + }); } async close(): Promise { - await this._channel.close(); + return this._wrapApiCall('browserServer.close', async () => { + await this._channel.close(); + }); } _checkLeaks() {} diff --git a/src/rpc/client/cdpSession.ts b/src/rpc/client/cdpSession.ts index e8d1948d72..467aaa22d4 100644 --- a/src/rpc/client/cdpSession.ts +++ b/src/rpc/client/cdpSession.ts @@ -46,11 +46,15 @@ export class CDPSession extends ChannelOwner { - const result = await this._channel.send({ method, params }); - return result.result as Protocol.CommandReturnValues[T]; + return this._wrapApiCall('cdpSession.send', async () => { + const result = await this._channel.send({ method, params }); + return result.result as Protocol.CommandReturnValues[T]; + }); } async detach() { - return this._channel.detach(); + return this._wrapApiCall('cdpSession.detach', async () => { + return this._channel.detach(); + }); } } diff --git a/src/rpc/client/channelOwner.ts b/src/rpc/client/channelOwner.ts index 611f4c58fc..b8c51036df 100644 --- a/src/rpc/client/channelOwner.ts +++ b/src/rpc/client/channelOwner.ts @@ -19,7 +19,7 @@ import { Channel } from '../channels'; import { Connection } from './connection'; import { assert } from '../../helper'; import { LoggerSink } from '../../loggerSink'; -import { rewriteErrorMessage } from '../../utils/stackTrace'; +import { DebugLoggerSink } from '../../logger'; export abstract class ChannelOwner extends EventEmitter { private _connection: Connection; @@ -99,19 +99,30 @@ export abstract class ChannelOwner(apiName: string, func: () => Promise, logger?: LoggerSink): Promise { + const stackObject: any = {}; + Error.captureStackTrace(stackObject); + const stack = stackObject.stack.startsWith('Error') ? stackObject.stack.substring(5) : stackObject.stack; logger = logger || this._logger; try { - if (logger && logger.isEnabled('api', 'info')) - logger.log('api', 'info', `=> ${apiName} started`, [], { color: 'cyan' }); + logApiCall(logger, `=> ${apiName} started`); const result = await func(); - if (logger && logger.isEnabled('api', 'info')) - logger.log('api', 'info', `=> ${apiName} succeeded`, [], { color: 'cyan' }); + logApiCall(logger, `<= ${apiName} succeeded`); return result; } catch (e) { - if (logger && logger.isEnabled('api', 'info')) - logger.log('api', 'info', `=> ${apiName} failed`, [], { color: 'cyan' }); - rewriteErrorMessage(e, `${apiName}: ` + e.message); + logApiCall(logger, `<= ${apiName} failed`); + // TODO: we could probably save "e.stack" in some log-heavy mode + // because it gives some insights into the server part. + e.message = `${apiName}: ` + e.message; + e.stack = e.message + stack; throw e; } } } + +const debugLogger = new DebugLoggerSink(); +function logApiCall(logger: LoggerSink | undefined, message: string) { + if (logger && logger.isEnabled('api', 'info')) + logger.log('api', 'info', message, [], { color: 'cyan' }); + if (debugLogger.isEnabled('api', 'info')) + debugLogger.log('api', 'info', message, [], { color: 'cyan' }); +} diff --git a/src/rpc/client/chromiumBrowser.ts b/src/rpc/client/chromiumBrowser.ts index 4c68b86147..d6646da93b 100644 --- a/src/rpc/client/chromiumBrowser.ts +++ b/src/rpc/client/chromiumBrowser.ts @@ -20,14 +20,20 @@ import { Browser } from './browser'; export class ChromiumBrowser extends Browser { async newBrowserCDPSession(): Promise { - return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session); + return this._wrapApiCall('chromiumBrowser.newBrowserCDPSession', async () => { + return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session); + }); } async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { - await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined }); + return this._wrapApiCall('chromiumBrowser.startTracing', async () => { + await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined }); + }); } async stopTracing(): Promise { - return Buffer.from((await this._channel.crStopTracing()).binary, 'base64'); + return this._wrapApiCall('chromiumBrowser.stopTracing', async () => { + return Buffer.from((await this._channel.crStopTracing()).binary, 'base64'); + }); } } diff --git a/src/rpc/client/chromiumBrowserContext.ts b/src/rpc/client/chromiumBrowserContext.ts index 0001f216ac..b2f9a2643e 100644 --- a/src/rpc/client/chromiumBrowserContext.ts +++ b/src/rpc/client/chromiumBrowserContext.ts @@ -51,7 +51,9 @@ export class ChromiumBrowserContext extends BrowserContext { } async newCDPSession(page: Page): Promise { - const result = await this._channel.crNewCDPSession({ page: page._channel }); - return CDPSession.from(result.session); + return this._wrapApiCall('chromiumBrowserContext.newCDPSession', async () => { + const result = await this._channel.crNewCDPSession({ page: page._channel }); + return CDPSession.from(result.session); + }); } } diff --git a/src/rpc/client/dialog.ts b/src/rpc/client/dialog.ts index 6d921a0eb7..c0117094f1 100644 --- a/src/rpc/client/dialog.ts +++ b/src/rpc/client/dialog.ts @@ -39,10 +39,14 @@ export class Dialog extends ChannelOwner { } async accept(promptText: string | undefined) { - await this._channel.accept({ promptText }); + return this._wrapApiCall('dialog.accept', async () => { + await this._channel.accept({ promptText }); + }); } async dismiss() { - await this._channel.dismiss(); + return this._wrapApiCall('dialog.dismiss', async () => { + await this._channel.dismiss(); + }); } } diff --git a/src/rpc/client/electron.ts b/src/rpc/client/electron.ts index 341276ce2d..8e34aa7ed2 100644 --- a/src/rpc/client/electron.ts +++ b/src/rpc/client/electron.ts @@ -25,6 +25,7 @@ import { TimeoutSettings } from '../../timeoutSettings'; import { Waiter } from './waiter'; import { TimeoutError } from '../../errors'; import { Events } from '../../events'; +import { LoggerSink } from '../../loggerSink'; export class Electron extends ChannelOwner { static from(electron: ElectronChannel): Electron { @@ -35,10 +36,12 @@ export class Electron extends ChannelOwner super(parent, type, guid, initializer, true); } - async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise { - options = { ...options }; - delete (options as any).logger; - return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication); + async launch(executablePath: string, options: ElectronLaunchOptions & { logger?: LoggerSink } = {}): Promise { + const logger = options.logger; + options = { ...options, logger: undefined }; + return this._wrapApiCall('electron.launch', async () => { + return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication); + }, logger); } } diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index d1f8946a06..98c1b7d8cd 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -259,7 +259,7 @@ export class Page extends ChannelOwner { } async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { - return await this._mainFrame.addStyleTag(options); + return this._attributeToPage(() => this._mainFrame.addStyleTag(options)); } async exposeFunction(name: string, playwrightFunction: Function) { @@ -267,16 +267,20 @@ export class Page extends ChannelOwner { } async exposeBinding(name: string, binding: FunctionWithSource) { - if (this._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered`); - if (this._browserContext._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered in the browser context`); - this._bindings.set(name, binding); - await this._channel.exposeBinding({ name }); + return this._wrapApiCall('page.exposeBinding', async () => { + if (this._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + if (this._browserContext._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered in the browser context`); + this._bindings.set(name, binding); + await this._channel.exposeBinding({ name }); + }); } async setExtraHTTPHeaders(headers: types.Headers) { - await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); + return this._wrapApiCall('page.setExtraHTTPHeaders', async () => { + await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }); + }); } url(): string { @@ -296,7 +300,9 @@ export class Page extends ChannelOwner { } async reload(options: types.NavigateOptions = {}): Promise { - return Response.fromNullable((await this._channel.reload(options)).response); + return this._wrapApiCall('page.reload', async () => { + return Response.fromNullable((await this._channel.reload(options)).response); + }); } async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise { @@ -340,20 +346,28 @@ export class Page extends ChannelOwner { } async goBack(options: types.NavigateOptions = {}): Promise { - return Response.fromNullable((await this._channel.goBack(options)).response); + return this._wrapApiCall('page.goBack', async () => { + return Response.fromNullable((await this._channel.goBack(options)).response); + }); } async goForward(options: types.NavigateOptions = {}): Promise { - return Response.fromNullable((await this._channel.goForward(options)).response); + return this._wrapApiCall('page.goForward', async () => { + return Response.fromNullable((await this._channel.goForward(options)).response); + }); } async emulateMedia(options: { media?: types.MediaType, colorScheme?: types.ColorScheme }) { - await this._channel.emulateMedia(options); + return this._wrapApiCall('page.emulateMedia', async () => { + await this._channel.emulateMedia(options); + }); } async setViewportSize(viewportSize: types.Size) { - this._viewportSize = viewportSize; - await this._channel.setViewportSize({ viewportSize }); + return this._wrapApiCall('page.setViewportSize', async () => { + this._viewportSize = viewportSize; + await this._channel.setViewportSize({ viewportSize }); + }); } viewportSize(): types.Size | null { @@ -368,20 +382,26 @@ export class Page extends ChannelOwner { } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - const source = await helper.evaluationScript(script, arg); - await this._channel.addInitScript({ source }); + return this._wrapApiCall('page.addInitScript', async () => { + const source = await helper.evaluationScript(script, arg); + await this._channel.addInitScript({ source }); + }); } async route(url: types.URLMatch, handler: RouteHandler): Promise { - this._routes.push({ url, handler }); - if (this._routes.length === 1) - await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + return this._wrapApiCall('page.route', async () => { + this._routes.push({ url, handler }); + if (this._routes.length === 1) + await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + }); } async unroute(url: types.URLMatch, handler?: RouteHandler): Promise { - this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); - if (this._routes.length === 0) - await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + return this._wrapApiCall('page.unroute', async () => { + this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); + if (this._routes.length === 0) + await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + }); } async screenshot(options: types.ScreenshotOptions = {}): Promise { @@ -395,9 +415,11 @@ export class Page extends ChannelOwner { } async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) { - await this._channel.close(options); - if (this._ownedContext) - await this._ownedContext.close(); + return this._wrapApiCall('page.close', async () => { + await this._channel.close(options); + if (this._ownedContext) + await this._ownedContext.close(); + }); } isClosed(): boolean { diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 9c7ebd17de..13253f6ae9 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -104,11 +104,11 @@ describe('BrowserContext', function() { }); it('should not allow deviceScaleFactor with null viewport', async({ browser }) => { const error = await browser.newContext({ viewport: null, deviceScaleFactor: 1 }).catch(e => e); - expect(error.message).toBe('"deviceScaleFactor" option is not supported with null "viewport"'); + expect(error.message).toContain('"deviceScaleFactor" option is not supported with null "viewport"'); }); it('should not allow isMobile with null viewport', async({ browser }) => { const error = await browser.newContext({ viewport: null, isMobile: true }).catch(e => e); - expect(error.message).toBe('"isMobile" option is not supported with null "viewport"'); + expect(error.message).toContain('"isMobile" option is not supported with null "viewport"'); }); it('close() should work for empty context', async({ browser }) => { const context = await browser.newContext(); @@ -393,13 +393,13 @@ describe('BrowserContext.exposeFunction', () => { await context.exposeFunction('foo', () => {}); await context.exposeFunction('bar', () => {}); let error = await context.exposeFunction('foo', () => {}).catch(e => e); - expect(error.message).toBe('Function "foo" has been already registered'); + expect(error.message).toContain('Function "foo" has been already registered'); const page = await context.newPage(); error = await page.exposeFunction('foo', () => {}).catch(e => e); - expect(error.message).toBe('Function "foo" has been already registered in the browser context'); + expect(error.message).toContain('Function "foo" has been already registered in the browser context'); await page.exposeFunction('baz', () => {}); error = await context.exposeFunction('baz', () => {}).catch(e => e); - expect(error.message).toBe('Function "baz" has been already registered in one of the pages'); + expect(error.message).toContain('Function "baz" has been already registered in one of the pages'); await context.close(); }); it('should be callable from-inside addInitScript', async({browser, server}) => { diff --git a/test/chromium/session.spec.js b/test/chromium/session.spec.js index ba31ed8fcb..ea7bfeb36e 100644 --- a/test/chromium/session.spec.js +++ b/test/chromium/session.spec.js @@ -66,7 +66,7 @@ describe('ChromiumBrowserContext.createSession', function() { } expect(error.message).toContain(CHANNEL ? 'Target browser or context has been closed' : 'Session closed.'); }); - it.skip(CHANNEL)('should throw nice errors', async function({page, browser}) { + it('should throw nice errors', async function({page, browser}) { const client = await page.context().newCDPSession(page); const error = await theSourceOfTheProblems().catch(error => error); expect(error.stack).toContain('theSourceOfTheProblems'); diff --git a/test/cookies.spec.js b/test/cookies.spec.js index 56710db54e..b898dbd709 100644 --- a/test/cookies.spec.js +++ b/test/cookies.spec.js @@ -369,7 +369,7 @@ describe('BrowserContext.addCookies', function() { } catch (e) { error = e; } - expect(error.message).toEqual( + expect(error.message).toContain( `Blank page can not have cookie "example-cookie-blank"` ); }); diff --git a/test/emulation.spec.js b/test/emulation.spec.js index 7e032df306..bdd87b916d 100644 --- a/test/emulation.spec.js +++ b/test/emulation.spec.js @@ -242,7 +242,7 @@ describe('Page.emulateMedia type', function() { it('should throw in case of bad type argument', async({page, server}) => { let error = null; await page.emulateMedia({ media: 'bad' }).catch(e => error = e); - expect(error.message).toBe('Unsupported media: bad'); + expect(error.message).toContain('Unsupported media: bad'); }); }); @@ -270,7 +270,7 @@ describe('Page.emulateMedia colorScheme', function() { it('should throw in case of bad argument', async({page, server}) => { let error = null; await page.emulateMedia({ colorScheme: 'bad' }).catch(e => error = e); - expect(error.message).toBe('Unsupported color scheme: bad'); + expect(error.message).toContain('Unsupported color scheme: bad'); }); it('should work during navigation', async({page, server}) => { await page.emulateMedia({ colorScheme: 'light' }); @@ -353,7 +353,7 @@ describe('BrowserContext({timezoneId})', function() { let error = null; const context = await browser.newContext({ timezoneId }); const page = await context.newPage().catch(e => error = e); - expect(error.message).toBe(`Invalid timezone ID: ${timezoneId}`); + expect(error.message).toContain(`Invalid timezone ID: ${timezoneId}`); await context.close(); } }); diff --git a/test/evaluation.jest.js b/test/evaluation.jest.js index f44818912d..0ba74cc75f 100644 --- a/test/evaluation.jest.js +++ b/test/evaluation.jest.js @@ -489,7 +489,7 @@ describe('Page.addInitScript', function() { }); it('should throw without path and content', async({page, server}) => { const error = await page.addInitScript({ foo: 'bar' }).catch(e => e); - expect(error.message).toBe('Either path or content property must be present'); + expect(error.message).toContain('Either path or content property must be present'); }); it('should work with browser context scripts', async({browser, server}) => { const context = await browser.newContext(); diff --git a/test/network.spec.js b/test/network.spec.js index f4bfd421d6..f726647b65 100644 --- a/test/network.spec.js +++ b/test/network.spec.js @@ -458,6 +458,6 @@ describe('Page.setExtraHTTPHeaders', function() { } catch (e) { error = e; } - expect(error.message).toBe('Expected value of header "foo" to be String, but "number" is found.'); + expect(error.message).toContain('Expected value of header "foo" to be String, but "number" is found.'); }); }); diff --git a/test/page.spec.js b/test/page.spec.js index d4333ffb82..58187085f1 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -101,7 +101,7 @@ describe('Page.Events.Load', function() { }); }); -describe.skip(CHANNEL)('Async stacks', () => { +describe('Async stacks', () => { it('should work', async({page, server}) => { server.setRoute('/empty.html', (req, res) => { req.socket.end(); diff --git a/test/permissions.spec.js b/test/permissions.spec.js index 5bf60dcd70..3b49fed30f 100644 --- a/test/permissions.spec.js +++ b/test/permissions.spec.js @@ -36,7 +36,7 @@ describe.skip(WEBKIT)('Permissions', function() { await page.goto(server.EMPTY_PAGE); let error = {}; await context.grantPermissions(['foo'], { origin: server.EMPTY_PAGE }).catch(e => error = e); - expect(error.message).toBe('Unknown permission: foo'); + expect(error.message).toContain('Unknown permission: foo'); }); it('should grant geolocation permission when listed', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); diff --git a/test/waittask.spec.js b/test/waittask.spec.js index de08285b19..7a88f75438 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -458,7 +458,7 @@ describe('Frame.waitForSelector', function() { await page.setContent(`
anything
`); expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything'); }); - it.skip(CHANNEL)('should have correct stack trace for timeout', async({page, server}) => { + it('should have correct stack trace for timeout', async({page, server}) => { let error; await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e); expect(error.stack).toContain('waittask.spec.js');