diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 322c3a5072d9d..483b2b293bfeb 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -239,6 +239,13 @@ Use [`method: Page.waitForLoadState`] to wait until the page gets to a particula cases). ::: +## event: BrowserContext.pageError +* since: v1.38 +- argument: <[PageError]> + +Emitted when unhandled exceptions occur on any pages created through this +context. To only listen for `pageError` events from a particular page, use [`event: Page.pageError`]. + ## event: BrowserContext.request * since: v1.12 - argument: <[Request]> diff --git a/docs/src/api/class-pageerror.md b/docs/src/api/class-pageerror.md new file mode 100644 index 0000000000000..d61829eae4b80 --- /dev/null +++ b/docs/src/api/class-pageerror.md @@ -0,0 +1,62 @@ +# class: PageError +* since: v1.38 + +[PageError] class represents objects created by context when there are unhandled +execeptions thrown on the pages and dispatched via the [`event: BrowserContext.pageError`] event. + +```js +// Log all uncaught errors to the terminal +context.on('pageerror', pageerror => { + console.log(`Uncaught exception: "${pageerror.error()}"`); +}); + +// Navigate to a page with an exception. +await page.goto('data:text/html,'); +``` + +```java +// Log all uncaught errors to the terminal +context.onPageError(pagerror -> { + System.out.println("Uncaught exception: " + pagerror.error()); +}); + +// Navigate to a page with an exception. +page.navigate("data:text/html,"); +``` + +```python async +# Log all uncaught errors to the terminal +context.on("pageerror", lambda pageerror: print(f"uncaught exception: {pageerror.error}")) + +# Navigate to a page with an exception. +await page.goto("data:text/html,") +``` + +```python sync +# Log all uncaught errors to the terminal +context.on("pageerror", lambda pageerror: print(f"uncaught exception: {pageerror.error}")) + +# Navigate to a page with an exception. +page.goto("data:text/html,") +``` + +```csharp +// Log all uncaught errors to the terminal +context.PageError += (_, pageerror) => +{ + Console.WriteLine("Uncaught exception: " + pageerror.Error); +}; +``` + +## method: PageError.page +* since: v1.38 +- returns: <[null]|[Page]> + +The page that produced this unhandled exception, if any. + +## method: PageError.error +* since: v1.38 +- returns: <[Error]> + +Unhandled error that was thrown. + diff --git a/packages/playwright-core/src/client/api.ts b/packages/playwright-core/src/client/api.ts index df14817104493..905c8543ce5e5 100644 --- a/packages/playwright-core/src/client/api.ts +++ b/packages/playwright-core/src/client/api.ts @@ -42,3 +42,4 @@ export { Video } from './video'; export { Worker } from './worker'; export { CDPSession } from './cdpSession'; export { Playwright } from './playwright'; +export { PageError } from './pageError'; diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 30144a1aaff67..3fee5a1dc20ec 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -41,6 +41,8 @@ import { rewriteErrorMessage } from '../utils/stackTrace'; import { HarRouter } from './harRouter'; import { ConsoleMessage } from './consoleMessage'; import { Dialog } from './dialog'; +import { PageError } from './pageError'; +import { parseError } from '../protocol/serializers'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); @@ -100,6 +102,13 @@ export class BrowserContext extends ChannelOwner if (page) page.emit(Events.Page.Console, consoleMessage); }); + this._channel.on('pageError', ({ error, page }) => { + const pageObject = Page.from(page); + const parsedError = parseError(error); + this.emit(Events.BrowserContext.PageError, new PageError(pageObject, parsedError)); + if (pageObject) + pageObject.emit(Events.Page.PageError, parsedError); + }); this._channel.on('dialog', ({ dialog }) => { const dialogObject = Dialog.from(dialog); let hasListeners = this.emit(Events.BrowserContext.Dialog, dialogObject); diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index ceca0829d6e2f..72747e0f5115d 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -39,6 +39,9 @@ export const Events = { Close: 'close', Dialog: 'dialog', Page: 'page', + // Can't use just 'error' due to node.js special treatment of error events. + // @see https://nodejs.org/api/events.html#events_error_events + PageError: 'pageerror', BackgroundPage: 'backgroundpage', ServiceWorker: 'serviceworker', Request: 'request', diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 6373ebe779a4e..0d2a0e98e5ab6 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -23,7 +23,7 @@ import { isSafeCloseError, kBrowserOrContextClosedError } from '../common/errors import { urlMatches } from '../utils/network'; import { TimeoutSettings } from '../common/timeoutSettings'; import type * as channels from '@protocol/channels'; -import { parseError, serializeError } from '../protocol/serializers'; +import { serializeError } from '../protocol/serializers'; import { assert, headersObjectToArray, isObject, isRegExp, isString, LongStandingScope, urlMatchesEqual } from '../utils'; import { mkdirIfNeeded } from '../utils/fileUtils'; import { Accessibility } from './accessibility'; @@ -130,7 +130,6 @@ export class Page extends ChannelOwner implements api.Page this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple))); this._channel.on('frameAttached', ({ frame }) => this._onFrameAttached(Frame.from(frame))); this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame))); - this._channel.on('pageError', ({ error }) => this.emit(Events.Page.PageError, parseError(error))); this._channel.on('route', ({ route }) => this._onRoute(Route.from(route))); this._channel.on('video', ({ artifact }) => { const artifactObject = Artifact.from(artifact); diff --git a/packages/playwright-core/src/client/pageError.ts b/packages/playwright-core/src/client/pageError.ts new file mode 100644 index 0000000000000..71b0b329c512a --- /dev/null +++ b/packages/playwright-core/src/client/pageError.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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. + */ + +import type * as api from '../../types/types'; +import type { Page } from './page'; + +export class PageError implements api.PageError { + private _page: Page | null; + private _error: Error; + + constructor(page: Page | null, error: Error) { + this._page = page; + this._error = error; + } + + page() { + return this._page; + } + + error() { + return this._error; + } +} diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 273dab728464f..c40db1e2e36f3 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -770,6 +770,10 @@ scheme.BrowserContextDialogEvent = tObject({ scheme.BrowserContextPageEvent = tObject({ page: tChannel(['Page']), }); +scheme.BrowserContextPageErrorEvent = tObject({ + error: tType('SerializedError'), + page: tChannel(['Page']), +}); scheme.BrowserContextRouteEvent = tObject({ route: tChannel(['Route']), }); @@ -957,9 +961,6 @@ scheme.PageFrameAttachedEvent = tObject({ scheme.PageFrameDetachedEvent = tObject({ frame: tChannel(['Frame']), }); -scheme.PagePageErrorEvent = tObject({ - error: tType('SerializedError'), -}); scheme.PageRouteEvent = tObject({ route: tChannel(['Route']), }); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index ac6b418098bcc..a5a30904689e6 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -48,6 +48,9 @@ export abstract class BrowserContext extends SdkObject { Close: 'close', Dialog: 'dialog', Page: 'page', + // Can't use just 'error' due to node.js special treatment of error events. + // @see https://nodejs.org/api/events.html#events_error_events + PageError: 'pageerror', Request: 'request', Response: 'response', RequestFailed: 'requestfailed', diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 254b42dc3c319..54f5e4459d909 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -756,7 +756,7 @@ class FrameSession { const args = event.args.map(o => worker._existingExecutionContext!.createHandle(o)); this._page._addConsoleMessage(event.type, args, toConsoleMessageLocation(event.stackTrace)); }); - session.on('Runtime.exceptionThrown', exception => this._page.emit(Page.Events.PageError, exceptionToError(exception.exceptionDetails))); + session.on('Runtime.exceptionThrown', exception => this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exception.exceptionDetails), this._page)); // TODO: attribute workers to the right frame. this._networkManager.instrumentNetworkEvents({ session, workerFrame: this._page._frameManager.frame(this._targetId) ?? undefined }); } @@ -859,7 +859,7 @@ class FrameSession { } _handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) { - this._page.firePageError(exceptionToError(exceptionDetails)); + this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, exceptionToError(exceptionDetails), this._page); } async _onTargetCrashed() { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 69e5ed355e61b..3bd83d81dcc44 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -38,6 +38,7 @@ import { DialogDispatcher } from './dialogDispatcher'; import type { Page } from '../page'; import type { Dialog } from '../dialog'; import type { ConsoleMessage } from '../console'; +import { serializeError } from '../../protocol/serializers'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -84,6 +85,9 @@ export class BrowserContextDispatcher extends Dispatcher { + this._dispatchEvent('pageError', { error: serializeError(error), page: PageDispatcher.from(this, page) }); + }); this.addObjectListener(BrowserContext.Events.Console, (message: ConsoleMessage) => { if (this._shouldDispatchEvent(message.page(), 'console')) this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(PageDispatcher.from(this, message.page()), message) }); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 63ecc51b288de..2a40a27d18e06 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -19,7 +19,7 @@ import type { Frame } from '../frames'; import { Page, Worker } from '../page'; import type * as channels from '@protocol/channels'; import { Dispatcher, existingDispatcher } from './dispatcher'; -import { parseError, serializeError } from '../../protocol/serializers'; +import { parseError } from '../../protocol/serializers'; import { FrameDispatcher } from './frameDispatcher'; import { RequestDispatcher } from './networkDispatchers'; import { ResponseDispatcher } from './networkDispatchers'; @@ -85,7 +85,6 @@ export class PageDispatcher extends Dispatcher this._onFrameAttached(frame)); this.addObjectListener(Page.Events.FrameDetached, frame => this._onFrameDetached(frame)); - this.addObjectListener(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); this.addObjectListener(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) })); this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) })); this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(parentScope, artifact) })); diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 378d7c8683fa0..c599d0e3e33ce 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -246,7 +246,7 @@ export class FFPage implements PageDelegate { const error = new Error(message); error.stack = params.message + '\n' + params.stack.split('\n').filter(Boolean).map(a => a.replace(/([^@]*)@(.*)/, ' at $1 ($2)')).join('\n'); error.name = name; - this._page.firePageError(error); + this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, error, this._page); } _onConsole(payload: Protocol.Runtime.consolePayload) { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 588e77b2bd2cd..a0b98a9d3f4e5 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -125,9 +125,6 @@ export class Page extends SdkObject { Crash: 'crash', Download: 'download', FileChooser: 'filechooser', - // Can't use just 'error' due to node.js special treatment of error events. - // @see https://nodejs.org/api/events.html#events_error_events - PageError: 'pageerror', FrameAttached: 'frameattached', FrameDetached: 'framedetached', InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument', @@ -696,10 +693,6 @@ export class Page extends SdkObject { this._frameThrottler.recharge(); } - firePageError(error: Error) { - this.emit(Page.Events.PageError, error); - } - async hideHighlight() { await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {}))); } diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 5d0bb4e241ca7..a2b24c0677774 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -555,7 +555,7 @@ export class WKPage implements PageDelegate { error.stack = stack; error.name = name; - this._page.firePageError(error); + this._page.emitOnContextOnceInitialized(BrowserContext.Events.PageError, error, this._page); return; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index bf75aa146b0e3..aea8dca052af9 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -7636,6 +7636,13 @@ export interface BrowserContext { */ on(event: 'page', listener: (page: Page) => void): this; + /** + * Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + * events from a particular page, use + * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error). + */ + on(event: 'pageerror', listener: (pageError: PageError) => void): this; + /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -7706,6 +7713,11 @@ export interface BrowserContext { */ once(event: 'page', listener: (page: Page) => void): this; + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'pageerror', listener: (pageError: PageError) => void): this; + /** * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. */ @@ -7817,6 +7829,13 @@ export interface BrowserContext { */ addListener(event: 'page', listener: (page: Page) => void): this; + /** + * Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + * events from a particular page, use + * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error). + */ + addListener(event: 'pageerror', listener: (pageError: PageError) => void): this; + /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -7887,6 +7906,11 @@ export interface BrowserContext { */ removeListener(event: 'page', listener: (page: Page) => void): this; + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'pageerror', listener: (pageError: PageError) => void): this; + /** * Removes an event listener added by `on` or `addListener`. */ @@ -7937,6 +7961,11 @@ export interface BrowserContext { */ off(event: 'page', listener: (page: Page) => void): this; + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'pageerror', listener: (pageError: PageError) => void): this; + /** * Removes an event listener added by `on` or `addListener`. */ @@ -8048,6 +8077,13 @@ export interface BrowserContext { */ prependListener(event: 'page', listener: (page: Page) => void): this; + /** + * Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + * events from a particular page, use + * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error). + */ + prependListener(event: 'pageerror', listener: (pageError: PageError) => void): this; + /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -8634,6 +8670,13 @@ export interface BrowserContext { */ waitForEvent(event: 'page', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; + /** + * Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + * events from a particular page, use + * [page.on('pageerror')](https://playwright.dev/docs/api/class-page#page-event-page-error). + */ + waitForEvent(event: 'pageerror', optionsOrPredicate?: { predicate?: (pageError: PageError) => boolean | Promise, timeout?: number } | ((pageError: PageError) => boolean | Promise)): Promise; + /** * Emitted when a request is issued from any pages created through this context. The [request] object is read-only. To * only listen for requests from a particular page, use @@ -17900,6 +17943,35 @@ export interface Mouse { wheel(deltaX: number, deltaY: number): Promise; } +/** + * {@link PageError} class represents objects created by context when there are unhandled execeptions thrown on the + * pages and dispatched via the + * [browserContext.on('pageerror')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-page-error) + * event. + * + * ```js + * // Log all uncaught errors to the terminal + * context.on('pageerror', pageerror => { + * console.log(`Uncaught exception: "${pageerror.error()}"`); + * }); + * + * // Navigate to a page with an exception. + * await page.goto('data:text/html,'); + * ``` + * + */ +export interface PageError { + /** + * Unhandled error that was thrown. + */ + error(): Error; + + /** + * The page that produced this unhandled exception, if any. + */ + page(): null|Page; +} + /** * This object can be used to launch or connect to Chromium, returning instances of {@link Browser}. */ diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 3ffa80dc60f4f..ee616ab07bd72 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1404,6 +1404,7 @@ export interface BrowserContextEventTarget { on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this; on(event: 'dialog', callback: (params: BrowserContextDialogEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; + on(event: 'pageError', callback: (params: BrowserContextPageErrorEvent) => void): this; on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; on(event: 'video', callback: (params: BrowserContextVideoEvent) => void): this; on(event: 'backgroundPage', callback: (params: BrowserContextBackgroundPageEvent) => void): this; @@ -1453,6 +1454,10 @@ export type BrowserContextDialogEvent = { export type BrowserContextPageEvent = { page: PageChannel, }; +export type BrowserContextPageErrorEvent = { + error: SerializedError, + page: PageChannel, +}; export type BrowserContextRouteEvent = { route: RouteChannel, }; @@ -1697,6 +1702,7 @@ export interface BrowserContextEvents { 'close': BrowserContextCloseEvent; 'dialog': BrowserContextDialogEvent; 'page': BrowserContextPageEvent; + 'pageError': BrowserContextPageErrorEvent; 'route': BrowserContextRouteEvent; 'video': BrowserContextVideoEvent; 'backgroundPage': BrowserContextBackgroundPageEvent; @@ -1725,7 +1731,6 @@ export interface PageEventTarget { on(event: 'fileChooser', callback: (params: PageFileChooserEvent) => void): this; on(event: 'frameAttached', callback: (params: PageFrameAttachedEvent) => void): this; on(event: 'frameDetached', callback: (params: PageFrameDetachedEvent) => void): this; - on(event: 'pageError', callback: (params: PagePageErrorEvent) => void): this; on(event: 'route', callback: (params: PageRouteEvent) => void): this; on(event: 'video', callback: (params: PageVideoEvent) => void): this; on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this; @@ -1787,9 +1792,6 @@ export type PageFrameAttachedEvent = { export type PageFrameDetachedEvent = { frame: FrameChannel, }; -export type PagePageErrorEvent = { - error: SerializedError, -}; export type PageRouteEvent = { route: RouteChannel, }; @@ -2220,7 +2222,6 @@ export interface PageEvents { 'fileChooser': PageFileChooserEvent; 'frameAttached': PageFrameAttachedEvent; 'frameDetached': PageFrameDetachedEvent; - 'pageError': PagePageErrorEvent; 'route': PageRouteEvent; 'video': PageVideoEvent; 'webSocket': PageWebSocketEvent; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index bbc4009c4b831..ed7dd92e6913f 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1188,6 +1188,11 @@ BrowserContext: parameters: page: Page + pageError: + parameters: + error: SerializedError + page: Page + route: parameters: route: Route @@ -1635,10 +1640,6 @@ Page: parameters: frame: Frame - pageError: - parameters: - error: SerializedError - route: parameters: route: Route diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts index b6f84aafc72e3..966d1d0852d7f 100644 --- a/tests/library/browsercontext-events.spec.ts +++ b/tests/library/browsercontext-events.spec.ts @@ -160,3 +160,12 @@ test('dialog event should work with inline script tag', async ({ page, server }) await promise; await expect.poll(() => popup.evaluate('window.result')).toBe('hello'); }); + +test('pageError event should work', async ({ page }) => { + const [pageerror] = await Promise.all([ + page.context().waitForEvent('pageerror'), + page.setContent(''), + ]); + expect(pageerror.page()).toBe(page); + expect(pageerror.error().stack).toContain('boom'); +});