feat: introduce page.on('crash') event (#1782)

Currently, whenever the page crashes, it emits an `'error'` event.
Error event is a special type of event in node.js; if unhandled,
it crashes the process.

Instead of emitting `'error'` event, this patch switches to emitting
`'crash'` event. Playwright users are free to handle the event
however they like, or just to ignore it.
This commit is contained in:
Andrey Lushnikov 2020-04-15 00:04:35 -07:00 committed by GitHub
parent aeabf9d707
commit 0ba823dd6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 50 additions and 13 deletions

View File

@ -632,6 +632,7 @@ page.removeListener('request', logRequest);
<!-- GEN:toc -->
- [event: 'close'](#event-close-1)
- [event: 'console'](#event-console)
- [event: 'crash'](#event-crash)
- [event: 'dialog'](#event-dialog)
- [event: 'domcontentloaded'](#event-domcontentloaded)
- [event: 'download'](#event-download)
@ -726,6 +727,10 @@ page.on('console', msg => {
page.evaluate(() => console.log('hello', 5, {foo: 'bar'}));
```
#### event: 'crash'
Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory.
#### event: 'dialog'
- <[Dialog]>

View File

@ -121,6 +121,7 @@ export class CRSession extends EventEmitter {
private readonly _targetType: string;
private readonly _sessionId: string;
private readonly _rootSessionId: string;
private _crashed: boolean = false;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
@ -141,10 +142,16 @@ export class CRSession extends EventEmitter {
this.once = super.once;
}
_markAsCrashed() {
this._crashed = true;
}
async send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed)
throw new Error('Target crashed');
if (!this._connection)
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
const id = this._connection._rawSend(this._sessionId, method, params);

View File

@ -613,7 +613,8 @@ class FrameSession {
this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails));
}
_onTargetCrashed() {
async _onTargetCrashed() {
this._client._markAsCrashed();
this._page._didCrash();
}

View File

@ -31,6 +31,7 @@ export const Events = {
Page: {
Close: 'close',
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
Download: 'download',

View File

@ -140,6 +140,7 @@ export class FFSession extends EventEmitter {
private _targetType: string;
private _sessionId: string;
private _rawSend: (message: any) => void;
private _crashed: boolean = false;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
@ -161,10 +162,16 @@ export class FFSession extends EventEmitter {
this.once = super.once;
}
markAsCrashed() {
this._crashed = true;
}
async send<T extends keyof Protocol.CommandParameters>(
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed)
throw new Error('Page crashed');
if (this._disposed)
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
const id = this._connection.nextMessageId();

View File

@ -251,6 +251,7 @@ export class FFPage implements PageDelegate {
}
async _onCrashed(event: Protocol.Page.crashedPayload) {
this._session.markAsCrashed();
this._page._didCrash();
}

View File

@ -159,10 +159,7 @@ export class Page extends ExtendedEventEmitter {
}
_didCrash() {
const error = new Error('Page crashed!');
// Do not report node.js stack.
error.stack = 'Error: ' + error.message; // Stack is supposed to contain error message as the first line.
this.emit('error', error);
this.emit(Events.Page.Crash);
}
_didDisconnect() {

View File

@ -97,6 +97,7 @@ export class WKSession extends EventEmitter {
private _disposed = false;
private readonly _rawSend: (message: any) => void;
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private _crashed: boolean = false;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
@ -122,6 +123,8 @@ export class WKSession extends EventEmitter {
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed)
throw new Error('Target crashed');
if (this._disposed)
throw new Error(`Protocol error (${method}): ${this.errorText}`);
const id = this.connection.nextMessageId();
@ -133,6 +136,10 @@ export class WKSession extends EventEmitter {
});
}
markAsCrashed() {
this._crashed = true;
}
isDisposed(): boolean {
return this._disposed;
}

View File

@ -194,8 +194,10 @@ export class WKPage implements PageDelegate {
} else if (this._session.sessionId === targetId) {
this._session.dispose();
helper.removeEventListeners(this._sessionListeners);
if (crashed)
if (crashed) {
this._session.markAsCrashed();
this._page._didCrash();
}
}
}

View File

@ -104,19 +104,28 @@ describe('Async stacks', () => {
});
});
describe('Page.Events.error', function() {
it('should throw when page crashes', async({page}) => {
await page.setContent(`<div>This page should crash</div>`);
let error = null;
page.on('error', err => error = err);
describe('Page.Events.Crash', function() {
function crash(page) {
if (CHROMIUM)
page.goto('chrome://crash').catch(e => {});
else if (WEBKIT)
page._delegate._session.send('Page.crash', {}).catch(e => {});
else if (FFOX)
page._delegate._session.send('Page.crash', {}).catch(e => {});
await new Promise(f => page.on('error', f));
expect(error.message).toBe('Page crashed!');
}
it('should emit crash event when page crashes', async({page}) => {
await page.setContent(`<div>This page should crash</div>`);
crash(page);
await new Promise(f => page.on('crash', f));
});
it('should throw on any action after page crashes', async({page}) => {
await page.setContent(`<div>This page should crash</div>`);
crash(page);
await page.waitForEvent('crash');
const err = await page.evaluate(() => {}).then(() => null, e => e);
expect(err).toBeTruthy();
expect(err.message).toContain('crash');
});
});