From f212fd1a8318345147c7357ced36db21acd477a0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 12 Oct 2023 11:05:34 -0700 Subject: [PATCH] chore: unify target closed errors (#27540) --- examples/todomvc/playwright.config.ts | 4 +-- examples/todomvc/tests/integration.spec.ts | 3 +- packages/playwright-core/src/cli/program.ts | 3 +- .../playwright-core/src/client/android.ts | 14 +++++----- .../playwright-core/src/client/browser.ts | 6 ++-- .../src/client/browserContext.ts | 3 +- .../playwright-core/src/client/browserType.ts | 7 ++--- .../playwright-core/src/client/connection.ts | 28 ++++++++++--------- .../playwright-core/src/client/electron.ts | 3 +- packages/playwright-core/src/client/fetch.ts | 4 +-- packages/playwright-core/src/client/frame.ts | 5 ++-- .../playwright-core/src/client/network.ts | 3 +- packages/playwright-core/src/client/page.ts | 10 +++---- packages/playwright-core/src/client/worker.ts | 4 +-- packages/playwright-core/src/common/errors.ts | 14 +++++++--- .../src/server/chromium/crConnection.ts | 11 ++++---- .../src/server/chromium/crPage.ts | 3 +- .../src/server/dispatchers/dispatcher.ts | 28 ++++--------------- .../src/server/firefox/ffBrowser.ts | 4 +-- .../src/server/firefox/ffConnection.ts | 11 ++++---- .../src/server/firefox/ffPage.ts | 3 +- .../src/server/frameSelectors.ts | 4 +-- packages/playwright-core/src/server/frames.ts | 2 +- packages/playwright-core/src/server/helper.ts | 2 +- packages/playwright-core/src/server/page.ts | 3 +- .../src/server/webkit/wkBrowser.ts | 6 ++-- .../src/server/webkit/wkConnection.ts | 8 +++--- .../src/server/webkit/wkPage.ts | 7 +++-- .../src/utils/manualPromise.ts | 13 +++++---- .../playwright-core/src/utils/stackTrace.ts | 10 +++++++ packages/playwright/src/util.ts | 12 ++------ tests/config/errors.ts | 17 +++++++++++ tests/library/browser.spec.ts | 3 +- tests/library/browsercontext-basic.spec.ts | 3 +- tests/library/browsertype-connect.spec.ts | 19 ++++++------- tests/library/browsertype-launch.spec.ts | 2 +- tests/library/chromium/session.spec.ts | 3 +- tests/library/web-socket.spec.ts | 3 +- .../elementhandle-eval-on-selector.spec.ts | 2 +- tests/page/eval-on-selector.spec.ts | 2 +- tests/page/page-basic.spec.ts | 7 +++-- tests/page/page-fill.spec.ts | 2 +- tests/page/selectors-frame.spec.ts | 8 +++--- tests/page/workers.spec.ts | 4 ++- tests/playwright-test/playwright.spec.ts | 2 +- tests/playwright-test/reporter.spec.ts | 2 +- 46 files changed, 171 insertions(+), 146 deletions(-) create mode 100644 tests/config/errors.ts diff --git a/examples/todomvc/playwright.config.ts b/examples/todomvc/playwright.config.ts index 06e262f2f0..f127e1a7f1 100644 --- a/examples/todomvc/playwright.config.ts +++ b/examples/todomvc/playwright.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ testDir: './tests', /* Maximum time one test can run for. */ - timeout: 30 * 1000, + timeout: 15_000, expect: { @@ -18,7 +18,7 @@ export default defineConfig({ * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000 + timeout: 5_000 }, /* Fail the build on CI if you accidentally left test.only in the source code. */ diff --git a/examples/todomvc/tests/integration.spec.ts b/examples/todomvc/tests/integration.spec.ts index 16c51e2b08..007896bb2c 100644 --- a/examples/todomvc/tests/integration.spec.ts +++ b/examples/todomvc/tests/integration.spec.ts @@ -17,9 +17,8 @@ const TODO_ITEMS = [ test.describe('New Todo', () => { test('should allow me to add todo items', async ({ page }) => { - test.setTimeout(5000); // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be completed?'); + const newTodo = page.getByPlaceholder('What needs to be done?'); // Create 1st todo. await newTodo.fill(TODO_ITEMS[0]); await newTodo.press('Enter'); diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 5fd3f385c4..8616e3bbcd 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -34,6 +34,7 @@ import { spawn } from 'child_process'; import { wrapInASCIIBox, isLikelyNpxGlobal, assert, gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils'; import type { Executable } from '../server'; import { registry, writeDockerVersion } from '../server'; +import { isTargetClosedError } from '../common/errors'; const packageJSON = require('../../package.json'); @@ -530,7 +531,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:')) url = 'http://' + url; await page.goto(url).catch(error => { - if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && error.message.includes('Navigation failed because page was closed')) { + if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && isTargetClosedError(error)) { // Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting // in a stray navigation aborted error. We should ignore it. } else { diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index a6163b53c7..45403acaa2 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -27,7 +27,7 @@ import { TimeoutSettings } from '../common/timeoutSettings'; import { Waiter } from './waiter'; import { EventEmitter } from 'events'; import { Connection } from './connection'; -import { isSafeCloseError, kBrowserClosedError } from '../common/errors'; +import { isTargetClosedError, TargetClosedError } from '../common/errors'; import { raceAgainstDeadline } from '../utils/timeoutRunner'; import type { AndroidServerLauncherImpl } from '../androidServerImpl'; @@ -76,10 +76,10 @@ export class Android extends ChannelOwner implements ap connection.on('close', closePipe); let device: AndroidDevice; - let closeError: string | undefined; + let closeError: Error | undefined; const onPipeClosed = () => { device?._didClose(); - connection.close(closeError || kBrowserClosedError); + connection.close(closeError); }; pipe.on('closed', onPipeClosed); connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed); @@ -88,7 +88,7 @@ export class Android extends ChannelOwner implements ap try { connection!.dispatch(message); } catch (e) { - closeError = e.toString(); + closeError = e; closePipe(); } }); @@ -237,11 +237,11 @@ export class AndroidDevice extends ChannelOwner i async close() { try { if (this._shouldCloseConnectionOnClose) - this._connection.close(kBrowserClosedError); + this._connection.close(); else await this._channel.close(); } catch (e) { - if (isSafeCloseError(e)) + if (isTargetClosedError(e)) return; throw e; } @@ -281,7 +281,7 @@ export class AndroidDevice extends ChannelOwner i const waiter = Waiter.createForEvent(this, event); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); if (event !== Events.AndroidDevice.Close) - waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed')); + waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new TargetClosedError()); const result = await waiter.waitForEvent(this, event, predicate as any); waiter.dispose(); return result; diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 279902de5c..3025dc2621 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -21,7 +21,7 @@ import type { Page } from './page'; import { ChannelOwner } from './channelOwner'; import { Events } from './events'; import type { LaunchOptions, BrowserContextOptions, HeadersArray } from './types'; -import { isSafeCloseError, kBrowserClosedError } from '../common/errors'; +import { isTargetClosedError } from '../common/errors'; import type * as api from '../../types/types'; import { CDPSession } from './cdpSession'; import type { BrowserType } from './browserType'; @@ -133,12 +133,12 @@ export class Browser extends ChannelOwner implements ap async close(): Promise { try { if (this._shouldCloseConnectionOnClose) - this._connection.close(kBrowserClosedError); + this._connection.close(); else await this._channel.close(); await this._closedPromise; } catch (e) { - if (isSafeCloseError(e)) + if (isTargetClosedError(e)) return; throw e; } diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 6c316b1a36..2350f14040 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -44,6 +44,7 @@ import { ConsoleMessage } from './consoleMessage'; import { Dialog } from './dialog'; import { WebError } from './webError'; import { parseError } from '../protocol/serializers'; +import { TargetClosedError } from '../common/errors'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); @@ -343,7 +344,7 @@ export class BrowserContext extends ChannelOwner const waiter = Waiter.createForEvent(this, event); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); if (event !== Events.BrowserContext.Close) - waiter.rejectOnEvent(this, Events.BrowserContext.Close, new Error('Context closed')); + waiter.rejectOnEvent(this, Events.BrowserContext.Close, new TargetClosedError()); const result = await waiter.waitForEvent(this, event, predicate as any); waiter.dispose(); return result; diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index f99ba15611..cc9b4eeaf5 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -25,7 +25,6 @@ import type { ChildProcess } from 'child_process'; import { envObjectToArray } from './clientHelper'; import { assert, headersObjectToArray, monotonicTime } from '../utils'; import type * as api from '../../types/types'; -import { kBrowserClosedError } from '../common/errors'; import { raceAgainstDeadline } from '../utils/timeoutRunner'; import type { Playwright } from './playwright'; @@ -144,7 +143,7 @@ export class BrowserType extends ChannelOwner imple connection.on('close', closePipe); let browser: Browser; - let closeError: string | undefined; + let closeError: Error | undefined; const onPipeClosed = () => { // Emulate all pages, contexts and the browser closing upon disconnect. for (const context of browser?.contexts() || []) { @@ -153,7 +152,7 @@ export class BrowserType extends ChannelOwner imple context._onClose(); } browser?._didClose(); - connection.close(closeError || kBrowserClosedError); + connection.close(closeError); }; pipe.on('closed', onPipeClosed); connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed); @@ -162,7 +161,7 @@ export class BrowserType extends ChannelOwner imple try { connection!.dispatch(message); } catch (e) { - closeError = e.toString(); + closeError = e; closePipe(); } }); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index bc64cdba92..61ce98a8a8 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -35,7 +35,6 @@ import { WritableStream } from './writableStream'; import { debugLogger } from '../common/debugLogger'; import { SelectorsOwner } from './selectors'; import { Android, AndroidSocket, AndroidDevice } from './android'; -import { captureLibraryStackText } from '../utils/stackTrace'; import { Artifact } from './artifact'; import { EventEmitter } from 'events'; import { JsonPipe } from './jsonPipe'; @@ -45,6 +44,8 @@ import { Tracing } from './tracing'; import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator'; import { createInstrumentation } from './clientInstrumentation'; import type { ClientInstrumentation } from './clientInstrumentation'; +import { TargetClosedError } from '../common/errors'; +import { formatCallLog, rewriteErrorMessage } from '../utils'; class Root extends ChannelOwner { constructor(connection: Connection) { @@ -67,7 +68,7 @@ export class Connection extends EventEmitter { private _lastId = 0; private _callbacks = new Map void, reject: (a: Error) => void, apiName: string | undefined, type: string, method: string }>(); private _rootObject: Root; - private _closedErrorMessage: string | undefined; + private _closedError: Error | undefined; private _isRemote = false; private _localUtils?: LocalUtils; // Some connections allow resolving in-process dispatchers. @@ -110,8 +111,8 @@ export class Connection extends EventEmitter { } async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], wallTime: number | undefined): Promise { - if (this._closedErrorMessage) - throw new Error(this._closedErrorMessage); + if (this._closedError) + throw this._closedError; if (object._wasCollected) throw new Error('The object has been collected to prevent unbounded heap growth.'); @@ -132,10 +133,10 @@ export class Connection extends EventEmitter { } dispatch(message: object) { - if (this._closedErrorMessage) + if (this._closedError) return; - const { id, guid, method, params, result, error } = message as any; + const { id, guid, method, params, result, error, log } = message as any; if (id) { if (debugLogger.isEnabled('channel')) debugLogger.log('channel', ' & { env?: Env, @@ -120,7 +121,7 @@ export class ElectronApplication extends ChannelOwner implements api.Fr private _setupNavigationWaiter(options: { timeout?: number }): Waiter { const waiter = new Waiter(this._page!, ''); 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!')); + waiter.rejectImmediately(new TargetClosedError()); + waiter.rejectOnEvent(this._page!, Events.Page.Close, new TargetClosedError()); waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!')); waiter.rejectOnEvent(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this); const timeout = this._page!._timeoutSettings.navigationTimeout(options); diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 681777166b..dd201e9a80 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -34,6 +34,7 @@ import { MultiMap } from '../utils/multimap'; import { APIResponse } from './fetch'; import type { Serializable } from '../../types/structs'; import type { BrowserContext } from './browserContext'; +import { TargetClosedError } from '../common/errors'; export type NetworkCookie = { name: string, @@ -610,7 +611,7 @@ export class WebSocket extends ChannelOwner implement 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')); + waiter.rejectOnEvent(this._page, Events.Page.Close, new TargetClosedError()); const result = await waiter.waitForEvent(this, event, predicate as any); waiter.dispose(); return result; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index b963ab484d..f1c817b090 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -19,7 +19,7 @@ import fs from 'fs'; import path from 'path'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; -import { isSafeCloseError, kBrowserOrContextClosedError } from '../common/errors'; +import { isTargetClosedError, TargetClosedError, kTargetClosedErrorMessage } from '../common/errors'; import { urlMatches } from '../utils/network'; import { TimeoutSettings } from '../common/timeoutSettings'; import type * as channels from '@protocol/channels'; @@ -140,8 +140,8 @@ export class Page extends ChannelOwner implements api.Page this.coverage = new Coverage(this._channel); - this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(kBrowserOrContextClosedError)); - this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(kBrowserOrContextClosedError)); + this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(kTargetClosedErrorMessage)); + this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(kTargetClosedErrorMessage)); this._setEventToSubscriptionMapping(new Map([ [Events.Page.Console, 'console'], @@ -398,7 +398,7 @@ export class Page extends ChannelOwner implements api.Page if (event !== Events.Page.Crash) waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed')); if (event !== Events.Page.Close) - waiter.rejectOnEvent(this, Events.Page.Close, new Error('Page closed')); + waiter.rejectOnEvent(this, Events.Page.Close, new TargetClosedError()); const result = await waiter.waitForEvent(this, event, predicate as any); waiter.dispose(); return result; @@ -520,7 +520,7 @@ export class Page extends ChannelOwner implements api.Page else await this._channel.close(options); } catch (e) { - if (isSafeCloseError(e) && !options.runBeforeUnload) + if (isTargetClosedError(e) && !options.runBeforeUnload) return; throw e; } diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index 3a399323d1..bc3a2b3acf 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -23,7 +23,7 @@ import type { BrowserContext } from './browserContext'; import type * as api from '../../types/types'; import type * as structs from '../../types/structs'; import { LongStandingScope } from '../utils'; -import { kBrowserOrContextClosedError } from '../common/errors'; +import { kTargetClosedErrorMessage } from '../common/errors'; export class Worker extends ChannelOwner implements api.Worker { _page: Page | undefined; // Set for web workers. @@ -43,7 +43,7 @@ export class Worker extends ChannelOwner implements api. this._context._serviceWorkers.delete(this); this.emit(Events.Worker.Close, this); }); - this.once(Events.Worker.Close, () => this._closedScope.close(kBrowserOrContextClosedError)); + this.once(Events.Worker.Close, () => this._closedScope.close(kTargetClosedErrorMessage)); } url(): string { diff --git a/packages/playwright-core/src/common/errors.ts b/packages/playwright-core/src/common/errors.ts index d064129795..78d7abdec9 100644 --- a/packages/playwright-core/src/common/errors.ts +++ b/packages/playwright-core/src/common/errors.ts @@ -25,9 +25,15 @@ class CustomError extends Error { export class TimeoutError extends CustomError {} -export const kBrowserClosedError = 'Browser has been closed'; -export const kBrowserOrContextClosedError = 'Target page, context or browser has been closed'; +export const kTargetClosedErrorMessage = 'Target page, context or browser has been closed'; -export function isSafeCloseError(error: Error) { - return error.message.endsWith(kBrowserClosedError) || error.message.endsWith(kBrowserOrContextClosedError); +export class TargetClosedError extends Error { + constructor() { + super(kTargetClosedErrorMessage); + this.name = this.constructor.name; + } +} + +export function isTargetClosedError(error: Error) { + return error instanceof TargetClosedError || error.message.includes(kTargetClosedErrorMessage); } diff --git a/packages/playwright-core/src/server/chromium/crConnection.ts b/packages/playwright-core/src/server/chromium/crConnection.ts index 4e2c0d0f08..368970a6db 100644 --- a/packages/playwright-core/src/server/chromium/crConnection.ts +++ b/packages/playwright-core/src/server/chromium/crConnection.ts @@ -25,6 +25,7 @@ import { debugLogger } from '../../common/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { ProtocolError } from '../protocolError'; +import { kTargetClosedErrorMessage } from '../../common/errors'; export const ConnectionEvents = { Disconnected: Symbol('ConnectionEvents.Disconnected') @@ -140,12 +141,10 @@ export class CRSession extends EventEmitter { private _closedErrorMessage() { if (this._crashed) return 'Target crashed'; - if (this._connection._browserDisconnectedLogs !== undefined) - return `Browser closed.` + this._connection._browserDisconnectedLogs; - if (this._closed) - return `Target closed`; - if (this._connection._closed) - return 'Browser closed'; + if (this._connection._browserDisconnectedLogs) + return kTargetClosedErrorMessage + '\nBrowser logs: ' + this._connection._browserDisconnectedLogs; + if (this._closed || this._connection._closed) + return kTargetClosedErrorMessage; } async send( diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 5c2fe4c512..65c05700b1 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -45,6 +45,7 @@ import { platformToFontFamilies } from './defaultFontFamilies'; import type { Protocol } from './protocol'; import { VideoRecorder } from './videoRecorder'; import { BrowserContext } from '../browserContext'; +import { TargetClosedError } from '../../common/errors'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -574,7 +575,7 @@ class FrameSession { } dispose() { - this._firstNonInitialNavigationCommittedReject(new Error('Page closed')); + this._firstNonInitialNavigationCommittedReject(new TargetClosedError()); for (const childSession of this._childSessions) childSession.dispose(); if (this._parentSession) diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index 66c65a4da6..790e16d328 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -19,10 +19,9 @@ import type * as channels from '@protocol/channels'; import { serializeError } from '../../protocol/serializers'; import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator'; import { assert, isUnderTest, monotonicTime } from '../../utils'; -import { kBrowserOrContextClosedError } from '../../common/errors'; +import { TargetClosedError } from '../../common/errors'; import type { CallMetadata } from '../instrumentation'; import { SdkObject } from '../instrumentation'; -import { rewriteErrorMessage } from '../../utils/stackTrace'; import type { PlaywrightDispatcher } from './playwrightDispatcher'; import { eventsHelper } from '../..//utils/eventsHelper'; import type { RegisteredListener } from '../..//utils/eventsHelper'; @@ -262,7 +261,7 @@ export class DispatcherConnection { const { id, guid, method, params, metadata } = message as any; const dispatcher = this._dispatchers.get(guid); if (!dispatcher) { - this.onmessage({ id, error: serializeError(new Error(kBrowserOrContextClosedError)) }); + this.onmessage({ id, error: serializeError(new TargetClosedError()) }); return; } @@ -324,20 +323,13 @@ export class DispatcherConnection { } } - - let error: any; await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata); try { const result = await (dispatcher as any)[method](validParams, callMetadata); const validator = findValidator(dispatcher._type, method, 'Result'); callMetadata.result = validator(result, '', { tChannelImpl: this._tChannelImplToWire.bind(this), binary: this._isLocal ? 'buffer' : 'toBase64' }); } catch (e) { - // Dispatching error - // We want original, unmodified error in metadata. callMetadata.error = serializeError(e); - if (callMetadata.log.length) - rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log)); - error = serializeError(e); } finally { callMetadata.endTime = monotonicTime(); await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata); @@ -346,18 +338,10 @@ export class DispatcherConnection { const response: any = { id }; if (callMetadata.result) response.result = callMetadata.result; - if (error) - response.error = error; + if (callMetadata.error) { + response.error = callMetadata.error; + response.log = callMetadata.log; + } this.onmessage(response); } } - -function formatLogRecording(log: string[]): string { - if (!log.length) - return ''; - const header = ` logs `; - const headerLength = 60; - const leftLength = (headerLength - header.length) / 2; - const rightLength = headerLength - header.length - leftLength; - return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`; -} diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 7a9a24cff6..6df7b4807e 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { kBrowserClosedError } from '../../common/errors'; +import { kTargetClosedErrorMessage } from '../../common/errors'; import { assert } from '../../utils'; import type { BrowserOptions } from '../browser'; import { Browser } from '../browser'; @@ -159,7 +159,7 @@ export class FFBrowser extends Browser { _onDisconnect() { for (const video of this._idToVideo.values()) - video.artifact.reportFinished(kBrowserClosedError); + video.artifact.reportFinished(kTargetClosedErrorMessage); this._idToVideo.clear(); for (const ffPage of this._ffPages.values()) ffPage.didClose(); diff --git a/packages/playwright-core/src/server/firefox/ffConnection.ts b/packages/playwright-core/src/server/firefox/ffConnection.ts index 04cdebf7ae..e60a3f1aa3 100644 --- a/packages/playwright-core/src/server/firefox/ffConnection.ts +++ b/packages/playwright-core/src/server/firefox/ffConnection.ts @@ -24,6 +24,7 @@ import { debugLogger } from '../../common/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; import { ProtocolError } from '../protocolError'; +import { kTargetClosedErrorMessage } from '../../common/errors'; export const ConnectionEvents = { Disconnected: Symbol('Disconnected'), @@ -134,12 +135,10 @@ export class FFSession extends EventEmitter { private _closedErrorMessage() { if (this._crashed) return 'Target crashed'; - if (this._connection._browserDisconnectedLogs !== undefined) - return `Browser closed.` + this._connection._browserDisconnectedLogs; - if (this._disposed) - return `Target closed`; - if (this._connection._closed) - return 'Browser closed'; + if (this._connection._browserDisconnectedLogs) + return kTargetClosedErrorMessage + '\nBrowser logs: ' + this._connection._browserDisconnectedLogs; + if (this._disposed || this._connection._closed) + return kTargetClosedErrorMessage; } async send( diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index a47e590712..51182b9441 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -35,6 +35,7 @@ import { splitErrorMessage } from '../../utils/stackTrace'; import { debugLogger } from '../../common/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; import { BrowserContext } from '../browserContext'; +import { TargetClosedError } from '../../common/errors'; export const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -342,7 +343,7 @@ export class FFPage implements PageDelegate { } didClose() { - this._markAsError(new Error('Page closed')); + this._markAsError(new TargetClosedError()); this._session.dispose(); eventsHelper.removeEventListeners(this._eventListeners); this._networkManager.dispose(); diff --git a/packages/playwright-core/src/server/frameSelectors.ts b/packages/playwright-core/src/server/frameSelectors.ts index 7a11f8564c..951f9a8945 100644 --- a/packages/playwright-core/src/server/frameSelectors.ts +++ b/packages/playwright-core/src/server/frameSelectors.ts @@ -66,7 +66,7 @@ export class FrameSelectors { const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: true }, scope); // Be careful, |this.frame| can be different from |resolved.frame|. if (!resolved) - throw new Error(`Error: failed to find frame for selector "${selector}"`); + throw new Error(`Failed to find frame for selector "${selector}"`); return await resolved.injected.evaluateHandle((injected, { info, scope }) => { return injected.querySelectorAll(info.parsed, scope || document); }, { info: resolved.info, scope: resolved.scope }); @@ -76,7 +76,7 @@ export class FrameSelectors { const resolved = await this.resolveInjectedForSelector(selector); // Be careful, |this.frame| can be different from |resolved.frame|. if (!resolved) - throw new Error(`Error: failed to find frame for selector "${selector}"`); + throw new Error(`Failed to find frame for selector "${selector}"`); return await resolved.injected.evaluate((injected, { info }) => { return injected.querySelectorAll(info.parsed, document).length; }, { info: resolved.info }); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 008ed32407..8e65cc9067 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -834,7 +834,7 @@ export class Frame extends SdkObject { async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise { const handle = await this.selectors.query(selector, { strict }, scope); if (!handle) - throw new Error(`Error: failed to find element matching selector "${selector}"`); + throw new Error(`Failed to find element matching selector "${selector}"`); const result = await handle.evaluateExpression(expression, { isFunction }, arg); handle.dispose(); return result; diff --git a/packages/playwright-core/src/server/helper.ts b/packages/playwright-core/src/server/helper.ts index ab59987a40..38246c9fd6 100644 --- a/packages/playwright-core/src/server/helper.ts +++ b/packages/playwright-core/src/server/helper.ts @@ -98,7 +98,7 @@ class Helper { static formatBrowserLogs(logs: string[]) { if (!logs.length) return ''; - return '\n' + '='.repeat(20) + ' Browser output: ' + '='.repeat(20) + '\n' + logs.join('\n'); + return '\n' + logs.join('\n'); } } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index ebf68372dc..d2117aafa9 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -43,6 +43,7 @@ import type { TimeoutOptions } from '../common/types'; import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser'; import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers'; import type { SerializedValue } from './isomorphic/utilityScriptSerializers'; +import { kTargetClosedErrorMessage } from '../common/errors'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -275,7 +276,7 @@ export class Page extends SdkObject { this.emit(Page.Events.Close); this._closedPromise.resolve(); this.instrumentation.onPageClose(this); - this.openScope.close('Page closed'); + this.openScope.close(kTargetClosedErrorMessage); } _didCrash() { diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 10d2e034bb..490e7d5aff 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -30,7 +30,7 @@ import type { Protocol } from './protocol'; import type { PageProxyMessageReceivedPayload } from './wkConnection'; import { kPageProxyMessageReceived, WKConnection, WKSession } from './wkConnection'; import { WKPage } from './wkPage'; -import { kBrowserClosedError } from '../../common/errors'; +import { kTargetClosedErrorMessage } from '../../common/errors'; import type { SdkObject } from '../instrumentation'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15'; @@ -81,7 +81,7 @@ export class WKBrowser extends Browser { wkPage.didClose(); this._wkPages.clear(); for (const video of this._idToVideo.values()) - video.artifact.reportFinished(kBrowserClosedError); + video.artifact.reportFinished(kTargetClosedErrorMessage); this._idToVideo.clear(); this._didClose(); } @@ -165,7 +165,7 @@ export class WKBrowser extends Browser { context = this._defaultContext as WKBrowserContext; if (!context) return; - const pageProxySession = new WKSession(this._connection, pageProxyId, `Target closed`, (message: any) => { + const pageProxySession = new WKSession(this._connection, pageProxyId, kTargetClosedErrorMessage, (message: any) => { this._connection.rawSend({ ...message, pageProxyId }); }); const opener = event.openerId ? this._wkPages.get(event.openerId) : undefined; diff --git a/packages/playwright-core/src/server/webkit/wkConnection.ts b/packages/playwright-core/src/server/webkit/wkConnection.ts index 6d5d0d8488..71cb8435ae 100644 --- a/packages/playwright-core/src/server/webkit/wkConnection.ts +++ b/packages/playwright-core/src/server/webkit/wkConnection.ts @@ -24,7 +24,7 @@ import type { RecentLogsCollector } from '../../common/debugLogger'; import { debugLogger } from '../../common/debugLogger'; import type { ProtocolLogger } from '../types'; import { helper } from '../helper'; -import { kBrowserClosedError } from '../../common/errors'; +import { kTargetClosedErrorMessage } from '../../common/errors'; import { ProtocolError } from '../protocolError'; // WKPlaywright uses this special id to issue Browser.close command which we @@ -51,7 +51,7 @@ export class WKConnection { this._onDisconnect = onDisconnect; this._protocolLogger = protocolLogger; this._browserLogsCollector = browserLogsCollector; - this.browserSession = new WKSession(this, '', kBrowserClosedError, (message: any) => { + this.browserSession = new WKSession(this, '', kTargetClosedErrorMessage, (message: any) => { this.rawSend(message); }); this._transport.onmessage = this._dispatchMessage.bind(this); @@ -137,7 +137,7 @@ export class WKSession extends EventEmitter { if (this._crashed) throw new ProtocolError(true, 'Target crashed'); if (this._disposed) - throw new ProtocolError(true, `Target closed`); + throw new ProtocolError(true, kTargetClosedErrorMessage); const id = this.connection.nextMessageId(); const messageObj = { id, method, params }; this._rawSend(messageObj); @@ -160,7 +160,7 @@ export class WKSession extends EventEmitter { dispose() { if (this.connection._browserDisconnectedLogs) - this.errorText = 'Browser closed.' + this.connection._browserDisconnectedLogs; + this.errorText = kTargetClosedErrorMessage + '\nBrowser logs: ' + this.connection._browserDisconnectedLogs; for (const callback of this._callbacks.values()) { callback.error.sessionClosed = true; callback.reject(rewriteErrorMessage(callback.error, this.errorText)); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 1d91e96337..dbe8f8c394 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -45,6 +45,7 @@ import { WKWorkers } from './wkWorkers'; import { debugLogger } from '../../common/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; import { BrowserContext } from '../browserContext'; +import { TargetClosedError, kTargetClosedErrorMessage } from '../../common/errors'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -271,7 +272,7 @@ export class WKPage implements PageDelegate { this._provisionalPage.dispose(); this._provisionalPage = null; } - this._firstNonInitialNavigationCommittedReject(new Error('Page closed')); + this._firstNonInitialNavigationCommittedReject(new TargetClosedError()); this._page._didClose(); } @@ -303,7 +304,7 @@ export class WKPage implements PageDelegate { private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) { const { targetInfo } = event; - const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, `Target closed`, (message: any) => { + const session = new WKSession(this._pageProxySession.connection, targetInfo.targetId, kTargetClosedErrorMessage, (message: any) => { this._pageProxySession.send('Target.sendMessageToTarget', { message: JSON.stringify(message), targetId: targetInfo.targetId }).catch(e => { @@ -524,7 +525,7 @@ export class WKPage implements PageDelegate { async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { if (this._pageProxySession.isDisposed()) - throw new Error('Target closed'); + throw new TargetClosedError(); const pageProxyId = this._pageProxySession.sessionId; const result = await this._pageProxySession.connection.browserSession.send('Playwright.navigate', { url, pageProxyId, frameId: frame._id, referrer }); return { newDocumentId: result.loaderId }; diff --git a/packages/playwright-core/src/utils/manualPromise.ts b/packages/playwright-core/src/utils/manualPromise.ts index 9decdab02b..25d47c8f4b 100644 --- a/packages/playwright-core/src/utils/manualPromise.ts +++ b/packages/playwright-core/src/utils/manualPromise.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { rewriteErrorMessage } from './stackTrace'; +import { captureRawStack } from './stackTrace'; export class ManualPromise extends Promise { private _resolve!: (t: T) => void; @@ -59,7 +59,7 @@ export class ManualPromise extends Promise { export class LongStandingScope { private _terminateError: Error | undefined; private _terminateErrorMessage: string | undefined; - private _terminatePromises = new Map, Error>(); + private _terminatePromises = new Map, string[]>(); private _isClosed = false; reject(error: Error) { @@ -72,9 +72,10 @@ export class LongStandingScope { close(errorMessage: string) { this._isClosed = true; this._terminateErrorMessage = errorMessage; - for (const [p, e] of this._terminatePromises) { - rewriteErrorMessage(e, errorMessage); - p.resolve(e); + for (const [p, frames] of this._terminatePromises) { + const error = new Error(errorMessage); + error.stack = [error.name + ':' + errorMessage, ...frames].join('\n'); + p.resolve(error); } } @@ -100,7 +101,7 @@ export class LongStandingScope { terminatePromise.resolve(this._terminateError); if (this._terminateErrorMessage) terminatePromise.resolve(new Error(this._terminateErrorMessage)); - this._terminatePromises.set(terminatePromise, new Error('')); + this._terminatePromises.set(terminatePromise, captureRawStack()); try { return await Promise.race([ terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)), diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 81cbf8e98d..2d18cee62d 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -18,6 +18,7 @@ import path from 'path'; import { parseStackTraceLine } from '../utilsBundle'; import { isUnderTest } from './'; import type { StackFrame } from '@protocol/channels'; +import { colors } from '../utilsBundle'; export function rewriteErrorMessage(e: E, newMessage: string): E { const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); @@ -131,6 +132,15 @@ export function splitErrorMessage(message: string): { name: string, message: str }; } +export function formatCallLog(log: string[] | undefined): string { + if (!log || !log.some(l => !!l)) + return ''; + return ` +Call log: + ${colors.dim('- ' + (log || []).join('\n - '))} +`; +} + export type ExpectZone = { title: string; wallTime: number; diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 756d2e3b18..f4c2da22db 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -20,7 +20,8 @@ import type { StackFrame } from '@protocol/channels'; import util from 'util'; import path from 'path'; import url from 'url'; -import { colors, debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; +import { debug, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; +import { formatCallLog } from 'playwright-core/lib/utils'; import type { TestInfoError } from './../types/test'; import type { Location } from './../types/testReporter'; import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; @@ -213,14 +214,7 @@ export function getContainedPath(parentPath: string, subPath: string = ''): stri export const debugTest = debug('pw:test'); -export function callLogText(log: string[] | undefined): string { - if (!log) - return ''; - return ` -Call log: - ${colors.dim('- ' + (log || []).join('\n - '))} -`; -} +export const callLogText = formatCallLog; const folderToPackageJsonPath = new Map(); diff --git a/tests/config/errors.ts b/tests/config/errors.ts new file mode 100644 index 0000000000..075c9f82d6 --- /dev/null +++ b/tests/config/errors.ts @@ -0,0 +1,17 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const kTargetClosedErrorMessage = 'Target page, context or browser has been closed'; diff --git a/tests/library/browser.spec.ts b/tests/library/browser.spec.ts index c3fdc305d0..f201b55988 100644 --- a/tests/library/browser.spec.ts +++ b/tests/library/browser.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { kTargetClosedErrorMessage } from '../config/errors'; import { browserTest as test, expect } from '../config/browserTest'; test('should return browserType', function({ browser, browserType }) { @@ -59,5 +60,5 @@ test('should dispatch page.on(close) upon browser.close and reject evaluate', as await browser.close(); expect(closed).toBe(true); const error = await promise; - expect(error.message).toMatch(/(Target|Browser) closed/); + expect(error.message).toContain(kTargetClosedErrorMessage); }); diff --git a/tests/library/browsercontext-basic.spec.ts b/tests/library/browsercontext-basic.spec.ts index 6747a042b8..d2e6aac8e6 100644 --- a/tests/library/browsercontext-basic.spec.ts +++ b/tests/library/browsercontext-basic.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { kTargetClosedErrorMessage } from '../config/errors'; import { browserTest as it, expect } from '../config/browserTest'; import { attachFrame, verifyViewport } from '../config/utils'; @@ -129,7 +130,7 @@ it('close() should abort waitForEvent', async ({ browser }) => { const promise = context.waitForEvent('page').catch(e => e); await context.close(); const error = await promise; - expect(error.message).toContain('Context closed'); + expect(error.message).toContain(kTargetClosedErrorMessage); }); it('close() should be callable twice', async ({ browser }) => { diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 1e024a534d..89d58035de 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -27,6 +27,7 @@ import { parseTrace, suppressCertificateWarning } from '../config/utils'; import formidable from 'formidable'; import type { Browser, ConnectOptions } from 'playwright-core'; import { createHttpServer } from '../../packages/playwright-core/lib/utils/network'; +import { kTargetClosedErrorMessage } from '../config/errors'; type ExtraFixtures = { connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise, @@ -336,7 +337,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { ]); expect(browser.isConnected()).toBe(false); const error = await page.waitForNavigation().catch(e => e); - expect(error.message).toContain('Navigation failed because page was closed'); + expect(error.message).toContain(kTargetClosedErrorMessage); }); test('should reject navigation when browser closes', async ({ connect, startRemoteServer, server }) => { @@ -391,7 +392,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { ]); for (let i = 0; i < 2; i++) { const message = results[i].message; - expect(message).toContain('Page closed'); + expect(message).toContain(kTargetClosedErrorMessage); expect(message).not.toContain('Timeout'); } }); @@ -549,13 +550,11 @@ for (const kind of ['launchServer', 'run-server'] as const) { await disconnectedPromise; expect(browser.isConnected()).toBe(false); - const navMessage = (await navigationPromise).message; - expect(navMessage).toContain('Connection closed'); - expect(navMessage).toContain('Closed by'); - expect(navMessage).toContain(__filename); - expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed'); + const navError = await navigationPromise; + expect(navError.message).toContain(kTargetClosedErrorMessage); + expect((await waitForNavigationPromise).message).toContain(kTargetClosedErrorMessage); expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed'); - expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed'); + expect((await page.waitForNavigation().catch(e => e)).message).toContain(kTargetClosedErrorMessage); }); test('should be able to connect when the wsEndpoint is passed as an option', async ({ browserType, startRemoteServer }) => { @@ -894,9 +893,9 @@ test.describe('launchServer only', () => { expect(browser.isConnected()).toBe(false); expect((await navigationPromise).message).toContain('has been closed'); - expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed'); + expect((await waitForNavigationPromise).message).toContain(kTargetClosedErrorMessage); expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed'); - expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed'); + expect((await page.waitForNavigation().catch(e => e)).message).toContain(kTargetClosedErrorMessage); }); test('should be able to reconnect to a browser 12 times without warnings', async ({ connect, startRemoteServer, server }) => { diff --git a/tests/library/browsertype-launch.spec.ts b/tests/library/browsertype-launch.spec.ts index 112901a2e6..21d17d0573 100644 --- a/tests/library/browsertype-launch.spec.ts +++ b/tests/library/browsertype-launch.spec.ts @@ -64,7 +64,7 @@ it('should reject if launched browser fails immediately', async ({ mode, browser let waitError = null; await browserType.launch({ executablePath: asset('dummy_bad_browser_executable.js') }).catch(e => waitError = e); - expect(waitError.message).toContain('== logs =='); + expect(waitError.message).toContain('Browser logs:'); }); it('should reject if executable path is invalid', async ({ browserType, mode }) => { diff --git a/tests/library/chromium/session.spec.ts b/tests/library/chromium/session.spec.ts index e5c6830ab0..c2069a7a3c 100644 --- a/tests/library/chromium/session.spec.ts +++ b/tests/library/chromium/session.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { kTargetClosedErrorMessage } from '../../config/errors'; import { contextTest as it, expect } from '../../config/browserTest'; import { browserTest } from '../../config/browserTest'; @@ -139,7 +140,7 @@ browserTest('should reject protocol calls when page closes', async function({ br const promise = session.send('Runtime.evaluate', { expression: 'new Promise(() => {})', awaitPromise: true }).catch(e => e); await page.close(); const error1 = await promise; - expect(error1.message).toContain('Target closed'); + expect(error1.message).toContain(kTargetClosedErrorMessage); const error2 = await session.send('Runtime.evaluate', { expression: 'new Promise(() => {})', awaitPromise: true }).catch(e => e); expect(error2.message).toContain('Target page, context or browser has been closed'); await context.close(); diff --git a/tests/library/web-socket.spec.ts b/tests/library/web-socket.spec.ts index 92081fe892..179c332307 100644 --- a/tests/library/web-socket.spec.ts +++ b/tests/library/web-socket.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { kTargetClosedErrorMessage } from '../config/errors'; import { contextTest as it, expect } from '../config/browserTest'; import { Server as WebSocketServer } from 'ws'; @@ -196,7 +197,7 @@ it('should reject waitForEvent on page close', async ({ page, server }) => { ]); const error = ws.waitForEvent('framesent').catch(e => e); await page.close(); - expect((await error).message).toContain('Page closed'); + expect((await error).message).toContain(kTargetClosedErrorMessage); }); it('should turn off when offline', async ({ page }) => { diff --git a/tests/page/elementhandle-eval-on-selector.spec.ts b/tests/page/elementhandle-eval-on-selector.spec.ts index eaa54c4949..93faaa9cb8 100644 --- a/tests/page/elementhandle-eval-on-selector.spec.ts +++ b/tests/page/elementhandle-eval-on-selector.spec.ts @@ -37,7 +37,7 @@ it('should throw in case of missing selector', async ({ page, server }) => { await page.setContent(htmlContent); const elementHandle = await page.$('#myId'); const errorMessage = await elementHandle.$eval('.a', node => (node as HTMLElement).innerText).catch(error => error.message); - expect(errorMessage).toContain(`Error: failed to find element matching selector ".a"`); + expect(errorMessage).toContain(`elementHandle.$eval: Failed to find element matching selector ".a"`); }); it('should work for all', async ({ page, server }) => { diff --git a/tests/page/eval-on-selector.spec.ts b/tests/page/eval-on-selector.spec.ts index 572e1d59ad..8bc63a4273 100644 --- a/tests/page/eval-on-selector.spec.ts +++ b/tests/page/eval-on-selector.spec.ts @@ -99,7 +99,7 @@ it('should accept ElementHandles as arguments', async ({ page, server }) => { it('should throw error if no element is found', async ({ page, server }) => { let error = null; await page.$eval('section', e => e.id).catch(e => error = e); - expect(error.message).toContain('failed to find element matching selector "section"'); + expect(error.message).toContain('Failed to find element matching selector "section"'); }); it('should support >> syntax', async ({ page, server }) => { diff --git a/tests/page/page-basic.spec.ts b/tests/page/page-basic.spec.ts index 04c33a7e82..38cb5340c0 100644 --- a/tests/page/page-basic.spec.ts +++ b/tests/page/page-basic.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { kTargetClosedErrorMessage } from '../config/errors'; import { test as it, expect } from './pageTest'; it('should reject all promises when page is closed', async ({ page, isWebView2, isAndroid }) => { @@ -26,7 +27,7 @@ it('should reject all promises when page is closed', async ({ page, isWebView2, page.evaluate(() => new Promise(r => {})).catch(e => error = e), page.close(), ]); - expect(error.message).toContain('Target closed'); + expect(error.message).toContain(kTargetClosedErrorMessage); }); it('should set the page close state', async ({ page, isWebView2 }) => { @@ -59,7 +60,7 @@ it('should terminate network waiters', async ({ page, server, isAndroid, isWebVi ]); for (let i = 0; i < 2; i++) { const message = results[i].message; - expect(message).toContain('Page closed'); + expect(message).toContain(kTargetClosedErrorMessage); expect(message).not.toContain('Timeout'); } }); @@ -142,7 +143,7 @@ it('should fail with error upon disconnect', async ({ page, isAndroid, isWebView const waitForPromise = page.waitForEvent('download').catch(e => error = e); await page.close(); await waitForPromise; - expect(error.message).toContain('Page closed'); + expect(error.message).toContain(kTargetClosedErrorMessage); }); it('page.url should work', async ({ page, server }) => { diff --git a/tests/page/page-fill.spec.ts b/tests/page/page-fill.spec.ts index 17161937d3..1e1585bea3 100644 --- a/tests/page/page-fill.spec.ts +++ b/tests/page/page-fill.spec.ts @@ -198,7 +198,7 @@ it('should throw nice error without injected script stack when element is not an let error = null; await page.goto(server.PREFIX + '/input/textarea.html'); await page.fill('body', '').catch(e => error = e); - expect(error.message).toContain('page.fill: Error: Element is not an ,