Skip to content

Commit

Permalink
feat: add support for browserContext.on('pageerror') (#24452)
Browse files Browse the repository at this point in the history
+ fix #24466
+ Adds support for exposing the `pageerror` events via `browserContext`
API.
+ Helps with capturing the overall exceptions that are thrown outside of
the the current page and also captures the exceptions happens on other
windows/popups.
+ Keeps the API in sync with `context.on('request)',
context.on('console'), etc..`
  • Loading branch information
vigneshshanmugam committed Aug 17, 2023
1 parent 2c48970 commit adc9b2d
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 27 deletions.
7 changes: 7 additions & 0 deletions docs/src/api/class-browsercontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]>
Expand Down
62 changes: 62 additions & 0 deletions docs/src/api/class-pageerror.md
Original file line number Diff line number Diff line change
@@ -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,<script>throw new Error("Test")</script>');
```

```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,<script>throw new Error('Test')</script>");
```

```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,<script>throw new Error('test')</script>")
```

```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,<script>throw new Error('test')</script>")
```

```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.

1 change: 1 addition & 0 deletions packages/playwright-core/src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ export { Video } from './video';
export { Worker } from './worker';
export { CDPSession } from './cdpSession';
export { Playwright } from './playwright';
export { PageError } from './pageError';
9 changes: 9 additions & 0 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
Expand Down Expand Up @@ -100,6 +102,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
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);
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/client/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 1 addition & 2 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -130,7 +130,6 @@ export class Page extends ChannelOwner<channels.PageChannel> 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);
Expand Down
36 changes: 36 additions & 0 deletions packages/playwright-core/src/client/pageError.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 4 additions & 3 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
});
Expand Down Expand Up @@ -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']),
});
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true;
Expand Down Expand Up @@ -84,6 +85,9 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('close');
this._dispose();
});
this.addObjectListener(BrowserContext.Events.PageError, (error: Error, page: Page) => {
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) });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -85,7 +85,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}));
this.addObjectListener(Page.Events.FrameAttached, frame => 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) }));
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 0 additions & 7 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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(() => {})));
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading

0 comments on commit adc9b2d

Please sign in to comment.