feat: browserContext.on('console') (#21943)

This commit is contained in:
Dmitry Gozman 2023-03-27 16:35:05 -07:00 committed by GitHub
parent 525097d465
commit f502c72f2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 340 additions and 50 deletions

View file

@ -94,6 +94,66 @@ Emitted when Browser context gets closed. This might happen because of one of th
* Browser application is closed or crashed. * Browser application is closed or crashed.
* The [`method: Browser.close`] method was called. * The [`method: Browser.close`] method was called.
## event: BrowserContext.console
* since: v1.33
* langs:
- alias-java: consoleMessage
- argument: <[ConsoleMessage]>
Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning.
The arguments passed into `console.log` and the page are available on the [ConsoleMessage] event handler argument.
**Usage**
```js
context.on('console', async msg => {
const values = [];
for (const arg of msg.args())
values.push(await arg.jsonValue());
console.log(...values);
});
await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
```
```java
context.onConsoleMessage(msg -> {
for (int i = 0; i < msg.args().size(); ++i)
System.out.println(i + ": " + msg.args().get(i).jsonValue());
});
page.evaluate("() => console.log('hello', 5, { foo: 'bar' })");
```
```python async
async def print_args(msg):
values = []
for arg in msg.args:
values.append(await arg.json_value())
print(values)
context.on("console", print_args)
await page.evaluate("console.log('hello', 5, { foo: 'bar' })")
```
```python sync
def print_args(msg):
for arg in msg.args:
print(arg.json_value())
context.on("console", print_args)
page.evaluate("console.log('hello', 5, { foo: 'bar' })")
```
```csharp
context.Console += async (_, msg) =>
{
foreach (var arg in msg.Args)
Console.WriteLine(await arg.JsonValueAsync<object>());
};
await page.EvaluateAsync("console.log('hello', 5, { foo: 'bar' })");
```
## event: BrowserContext.page ## event: BrowserContext.page
* since: v1.8 * since: v1.8
- argument: <[Page]> - argument: <[Page]>

View file

@ -125,6 +125,12 @@ List of arguments passed to a `console` function call. See also [`event: Page.co
URL of the resource followed by 0-based line and column numbers in the resource formatted as `URL:line:column`. URL of the resource followed by 0-based line and column numbers in the resource formatted as `URL:line:column`.
## method: ConsoleMessage.page
* since: v1.33
- returns: <[Page]|[null]>
The page that produced this console message, if any.
## method: ConsoleMessage.text ## method: ConsoleMessage.text
* since: v1.8 * since: v1.8
- returns: <[string]> - returns: <[string]>

View file

@ -163,12 +163,11 @@ Emitted when the page closes.
- alias-java: consoleMessage - alias-java: consoleMessage
- argument: <[ConsoleMessage]> - argument: <[ConsoleMessage]>
Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning.
emitted if the page throws an error or a warning.
The arguments passed into `console.log` appear as arguments on the event handler. The arguments passed into `console.log` are available on the [ConsoleMessage] event handler argument.
An example of handling `console` event: **Usage**
```js ```js
page.on('console', async msg => { page.on('console', async msg => {
@ -177,7 +176,7 @@ page.on('console', async msg => {
values.push(await arg.jsonValue()); values.push(await arg.jsonValue());
console.log(...values); console.log(...values);
}); });
await page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
``` ```
```java ```java
@ -185,7 +184,7 @@ page.onConsoleMessage(msg -> {
for (int i = 0; i < msg.args().size(); ++i) for (int i = 0; i < msg.args().size(); ++i)
System.out.println(i + ": " + msg.args().get(i).jsonValue()); System.out.println(i + ": " + msg.args().get(i).jsonValue());
}); });
page.evaluate("() => console.log('hello', 5, {foo: 'bar'})"); page.evaluate("() => console.log('hello', 5, { foo: 'bar' })");
``` ```
```python async ```python async
@ -196,7 +195,7 @@ async def print_args(msg):
print(values) print(values)
page.on("console", print_args) page.on("console", print_args)
await page.evaluate("console.log('hello', 5, {foo: 'bar'})") await page.evaluate("console.log('hello', 5, { foo: 'bar' })")
``` ```
```python sync ```python sync
@ -205,7 +204,7 @@ def print_args(msg):
print(arg.json_value()) print(arg.json_value())
page.on("console", print_args) page.on("console", print_args)
page.evaluate("console.log('hello', 5, {foo: 'bar'})") page.evaluate("console.log('hello', 5, { foo: 'bar' })")
``` ```
```csharp ```csharp

View file

@ -40,6 +40,7 @@ import { APIRequestContext } from './fetch';
import { createInstrumentation } from './clientInstrumentation'; import { createInstrumentation } from './clientInstrumentation';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
import { HarRouter } from './harRouter'; import { HarRouter } from './harRouter';
import { ConsoleMessage } from './consoleMessage';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext { export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>(); _pages = new Set<Page>();
@ -92,6 +93,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._serviceWorkers.add(serviceWorker); this._serviceWorkers.add(serviceWorker);
this.emit(Events.BrowserContext.ServiceWorker, serviceWorker); this.emit(Events.BrowserContext.ServiceWorker, serviceWorker);
}); });
this._channel.on('console', ({ message }) => {
const consoleMessage = ConsoleMessage.from(message);
this.emit(Events.BrowserContext.Console, consoleMessage);
const page = consoleMessage.page();
if (page)
page.emit(Events.Page.Console, consoleMessage);
});
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page))); this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page))); this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
this._channel.on('requestFinished', params => this._onRequestFinished(params)); this._channel.on('requestFinished', params => this._onRequestFinished(params));

View file

@ -19,6 +19,7 @@ import { JSHandle } from './jsHandle';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import { Page } from './page';
type ConsoleMessageLocation = channels.ConsoleMessageInitializer['location']; type ConsoleMessageLocation = channels.ConsoleMessageInitializer['location'];
@ -27,8 +28,18 @@ export class ConsoleMessage extends ChannelOwner<channels.ConsoleMessageChannel>
return (message as any)._object; return (message as any)._object;
} }
private _page: Page | null;
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ConsoleMessageInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ConsoleMessageInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
// Note: currently, we only report console messages for pages and they always have a page.
// However, in the future we might report console messages for service workers or something else,
// where page() would be null.
this._page = Page.fromNullable(initializer.page);
}
page() {
return this._page;
} }
type(): string { type(): string {

View file

@ -35,6 +35,7 @@ export const Events = {
}, },
BrowserContext: { BrowserContext: {
Console: 'console',
Close: 'close', Close: 'close',
Page: 'page', Page: 'page',
BackgroundPage: 'backgroundpage', BackgroundPage: 'backgroundpage',

View file

@ -31,7 +31,6 @@ import { Artifact } from './artifact';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { evaluationScript } from './clientHelper'; import { evaluationScript } from './clientHelper';
import { ConsoleMessage } from './consoleMessage';
import { Coverage } from './coverage'; import { Coverage } from './coverage';
import { Dialog } from './dialog'; import { Dialog } from './dialog';
import { Download } from './download'; import { Download } from './download';
@ -124,7 +123,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding))); this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
this._channel.on('close', () => this._onClose()); this._channel.on('close', () => this._onClose());
this._channel.on('console', ({ message }) => this.emit(Events.Page.Console, ConsoleMessage.from(message)));
this._channel.on('crash', () => this._onCrash()); this._channel.on('crash', () => this._onCrash());
this._channel.on('dialog', ({ dialog }) => { this._channel.on('dialog', ({ dialog }) => {
const dialogObj = Dialog.from(dialog); const dialogObj = Dialog.from(dialog);

View file

@ -755,6 +755,9 @@ scheme.BrowserContextInitializer = tObject({
scheme.BrowserContextBindingCallEvent = tObject({ scheme.BrowserContextBindingCallEvent = tObject({
binding: tChannel(['BindingCall']), binding: tChannel(['BindingCall']),
}); });
scheme.BrowserContextConsoleEvent = tObject({
message: tChannel(['ConsoleMessage']),
});
scheme.BrowserContextCloseEvent = tOptional(tObject({})); scheme.BrowserContextCloseEvent = tOptional(tObject({}));
scheme.BrowserContextPageEvent = tObject({ scheme.BrowserContextPageEvent = tObject({
page: tChannel(['Page']), page: tChannel(['Page']),
@ -929,9 +932,6 @@ scheme.PageBindingCallEvent = tObject({
binding: tChannel(['BindingCall']), binding: tChannel(['BindingCall']),
}); });
scheme.PageCloseEvent = tOptional(tObject({})); scheme.PageCloseEvent = tOptional(tObject({}));
scheme.PageConsoleEvent = tObject({
message: tChannel(['ConsoleMessage']),
});
scheme.PageCrashEvent = tOptional(tObject({})); scheme.PageCrashEvent = tOptional(tObject({}));
scheme.PageDialogEvent = tObject({ scheme.PageDialogEvent = tObject({
dialog: tChannel(['Dialog']), dialog: tChannel(['Dialog']),
@ -2072,6 +2072,7 @@ scheme.WebSocketSocketErrorEvent = tObject({
}); });
scheme.WebSocketCloseEvent = tOptional(tObject({})); scheme.WebSocketCloseEvent = tOptional(tObject({}));
scheme.ConsoleMessageInitializer = tObject({ scheme.ConsoleMessageInitializer = tObject({
page: tChannel(['Page']),
type: tString, type: tString,
text: tString, text: tString,
args: tArray(tChannel(['ElementHandle', 'JSHandle'])), args: tArray(tChannel(['ElementHandle', 'JSHandle'])),

View file

@ -44,6 +44,7 @@ import type { Artifact } from './artifact';
export abstract class BrowserContext extends SdkObject { export abstract class BrowserContext extends SdkObject {
static Events = { static Events = {
Console: 'console',
Close: 'close', Close: 'close',
Page: 'page', Page: 'page',
Request: 'request', Request: 'request',

View file

@ -17,21 +17,28 @@
import { SdkObject } from './instrumentation'; import { SdkObject } from './instrumentation';
import type * as js from './javascript'; import type * as js from './javascript';
import type { ConsoleMessageLocation } from './types'; import type { ConsoleMessageLocation } from './types';
import type { Page } from './page';
export class ConsoleMessage extends SdkObject { export class ConsoleMessage extends SdkObject {
private _type: string; private _type: string;
private _text?: string; private _text?: string;
private _args: js.JSHandle[]; private _args: js.JSHandle[];
private _location: ConsoleMessageLocation; private _location: ConsoleMessageLocation;
private _page: Page;
constructor(parent: SdkObject, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { constructor(page: Page, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) {
super(parent, 'console-message'); super(page, 'console-message');
this._page = page;
this._type = type; this._type = type;
this._text = text; this._text = text;
this._args = args; this._args = args;
this._location = location || { url: '', lineNumber: 0, columnNumber: 0 }; this._location = location || { url: '', lineNumber: 0, columnNumber: 0 };
} }
page() {
return this._page;
}
type(): string { type(): string {
return this._type; return this._type;
} }

View file

@ -33,6 +33,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { createGuid, urlMatches } from '../../utils'; import { createGuid, urlMatches } from '../../utils';
import { WritableStreamDispatcher } from './writableStreamDispatcher'; import { WritableStreamDispatcher } from './writableStreamDispatcher';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true; _type_EventTarget = true;
@ -79,6 +80,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });
this.addObjectListener(BrowserContext.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this, message) }));
if (context._browser.options.name === 'chromium') { if (context._browser.options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages()) for (const page of (context as CRBrowserContext).backgroundPages())

View file

@ -16,19 +16,22 @@
import type { ConsoleMessage } from '../console'; import type { ConsoleMessage } from '../console';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import type { PageDispatcher } from './pageDispatcher'; import { PageDispatcher } from './pageDispatcher';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import { Dispatcher } from './dispatcher'; import { Dispatcher } from './dispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
export class ConsoleMessageDispatcher extends Dispatcher<ConsoleMessage, channels.ConsoleMessageChannel, PageDispatcher> implements channels.ConsoleMessageChannel { export class ConsoleMessageDispatcher extends Dispatcher<ConsoleMessage, channels.ConsoleMessageChannel, BrowserContextDispatcher> implements channels.ConsoleMessageChannel {
_type_ConsoleMessage = true; _type_ConsoleMessage = true;
constructor(scope: PageDispatcher, message: ConsoleMessage) { constructor(scope: BrowserContextDispatcher, message: ConsoleMessage) {
const page = PageDispatcher.from(scope, message.page());
super(scope, message, 'ConsoleMessage', { super(scope, message, 'ConsoleMessage', {
type: message.type(), type: message.type(),
text: message.text(), text: message.text(),
args: message.args().map(a => ElementHandleDispatcher.fromJSHandle(scope, a)), args: message.args().map(a => ElementHandleDispatcher.fromJSHandle(page, a)),
location: message.location(), location: message.location(),
page,
}); });
} }
} }

View file

@ -20,7 +20,6 @@ import { Page, Worker } from '../page';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { Dispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import { parseError, serializeError } from '../../protocol/serializers'; import { parseError, serializeError } from '../../protocol/serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher'; import { DialogDispatcher } from './dialogDispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import { RequestDispatcher } from './networkDispatchers'; import { RequestDispatcher } from './networkDispatchers';
@ -76,7 +75,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
this._dispatchEvent('close'); this._dispatchEvent('close');
this._dispose(); this._dispose();
}); });
this.addObjectListener(Page.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this, message) }));
this.addObjectListener(Page.Events.Crash, () => this._dispatchEvent('crash')); this.addObjectListener(Page.Events.Crash, () => this._dispatchEvent('crash'));
this.addObjectListener(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) })); this.addObjectListener(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) }));
this.addObjectListener(Page.Events.Download, (download: Download) => { this.addObjectListener(Page.Events.Download, (download: Download) => {

View file

@ -1028,8 +1028,8 @@ export class Frame extends SdkObject {
let cspMessage: ConsoleMessage | undefined; let cspMessage: ConsoleMessage | undefined;
const actionPromise = func().then(r => result = r).catch(e => error = e); const actionPromise = func().then(r => result = r).catch(e => error = e);
const errorPromise = new Promise<void>(resolve => { const errorPromise = new Promise<void>(resolve => {
listeners.push(eventsHelper.addEventListener(this._page, Page.Events.Console, (message: ConsoleMessage) => { listeners.push(eventsHelper.addEventListener(this._page._browserContext, BrowserContext.Events.Console, (message: ConsoleMessage) => {
if (message.type() === 'error' && message.text().includes('Content Security Policy')) { if (message.page() === this._page && message.type() === 'error' && message.text().includes('Content Security Policy')) {
cspMessage = message; cspMessage = message;
resolve(); resolve();
} }

View file

@ -121,7 +121,6 @@ export class Page extends SdkObject {
static Events = { static Events = {
Close: 'close', Close: 'close',
Crash: 'crash', Crash: 'crash',
Console: 'console',
Dialog: 'dialog', Dialog: 'dialog',
Download: 'download', Download: 'download',
FileChooser: 'filechooser', FileChooser: 'filechooser',
@ -141,6 +140,7 @@ export class Page extends SdkObject {
private _closedPromise = new ManualPromise<void>(); private _closedPromise = new ManualPromise<void>();
private _disconnected = false; private _disconnected = false;
private _initialized = false; private _initialized = false;
private _consoleMessagesBeforeInitialized: ConsoleMessage[] = [];
readonly _disconnectedPromise = new ManualPromise<Error>(); readonly _disconnectedPromise = new ManualPromise<Error>();
readonly _crashedPromise = new ManualPromise<Error>(); readonly _crashedPromise = new ManualPromise<Error>();
readonly _browserContext: BrowserContext; readonly _browserContext: BrowserContext;
@ -208,12 +208,18 @@ export class Page extends SdkObject {
} }
this._initialized = true; this._initialized = true;
this.emitOnContext(contextEvent, this); this.emitOnContext(contextEvent, this);
// I may happen that page initialization finishes after Close event has already been sent,
for (const message of this._consoleMessagesBeforeInitialized)
this.emitOnContext(BrowserContext.Events.Console, message);
this._consoleMessagesBeforeInitialized = [];
// It may happen that page initialization finishes after Close event has already been sent,
// in that case we fire another Close event to ensure that each reported Page will have // in that case we fire another Close event to ensure that each reported Page will have
// corresponding Close event after it is reported on the context. // corresponding Close event after it is reported on the context.
if (this.isClosed()) if (this.isClosed())
this.emit(Page.Events.Close); this.emit(Page.Events.Close);
this.instrumentation.onPageOpen(this); else
this.instrumentation.onPageOpen(this);
} }
initializedOrUndefined() { initializedOrUndefined() {
@ -351,10 +357,18 @@ export class Page extends SdkObject {
_addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { _addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(this, type, text, args, location); const message = new ConsoleMessage(this, type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message); const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Page.Events.Console)) if (intercepted) {
args.forEach(arg => arg.dispose()); args.forEach(arg => arg.dispose());
return;
}
// Console message may come before page is ready. In this case, postpone the message
// until page is initialized, and dispatch it to the client later, either on the live Page,
// or on the "errored" Page.
if (this._initialized)
this.emitOnContext(BrowserContext.Events.Console, message);
else else
this.emit(Page.Events.Console, message); this._consoleMessagesBeforeInitialized.push(message);
} }
async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> { async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {

View file

@ -878,9 +878,9 @@ export interface Page {
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning. * emitted if the page throws an error or a warning.
* *
* The arguments passed into `console.log` appear as arguments on the event handler. * The arguments passed into `console.log` are available on the [ConsoleMessage] event handler argument.
* *
* An example of handling `console` event: * **Usage**
* *
* ```js * ```js
* page.on('console', async msg => { * page.on('console', async msg => {
@ -889,7 +889,7 @@ export interface Page {
* values.push(await arg.jsonValue()); * values.push(await arg.jsonValue());
* console.log(...values); * console.log(...values);
* }); * });
* await page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); * await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ``` * ```
* *
*/ */
@ -1171,9 +1171,9 @@ export interface Page {
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning. * emitted if the page throws an error or a warning.
* *
* The arguments passed into `console.log` appear as arguments on the event handler. * The arguments passed into `console.log` are available on the [ConsoleMessage] event handler argument.
* *
* An example of handling `console` event: * **Usage**
* *
* ```js * ```js
* page.on('console', async msg => { * page.on('console', async msg => {
@ -1182,7 +1182,7 @@ export interface Page {
* values.push(await arg.jsonValue()); * values.push(await arg.jsonValue());
* console.log(...values); * console.log(...values);
* }); * });
* await page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); * await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ``` * ```
* *
*/ */
@ -1559,9 +1559,9 @@ export interface Page {
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning. * emitted if the page throws an error or a warning.
* *
* The arguments passed into `console.log` appear as arguments on the event handler. * The arguments passed into `console.log` are available on the [ConsoleMessage] event handler argument.
* *
* An example of handling `console` event: * **Usage**
* *
* ```js * ```js
* page.on('console', async msg => { * page.on('console', async msg => {
@ -1570,7 +1570,7 @@ export interface Page {
* values.push(await arg.jsonValue()); * values.push(await arg.jsonValue());
* console.log(...values); * console.log(...values);
* }); * });
* await page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); * await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ``` * ```
* *
*/ */
@ -4197,9 +4197,9 @@ export interface Page {
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning. * emitted if the page throws an error or a warning.
* *
* The arguments passed into `console.log` appear as arguments on the event handler. * The arguments passed into `console.log` are available on the [ConsoleMessage] event handler argument.
* *
* An example of handling `console` event: * **Usage**
* *
* ```js * ```js
* page.on('console', async msg => { * page.on('console', async msg => {
@ -4208,7 +4208,7 @@ export interface Page {
* values.push(await arg.jsonValue()); * values.push(await arg.jsonValue());
* console.log(...values); * console.log(...values);
* }); * });
* await page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); * await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ``` * ```
* *
*/ */
@ -7449,6 +7449,27 @@ export interface BrowserContext {
*/ */
on(event: 'close', listener: (browserContext: BrowserContext) => void): this; on(event: 'close', listener: (browserContext: BrowserContext) => void): this;
/**
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning.
*
* The arguments passed into `console.log` and the page are available on the [ConsoleMessage] event handler argument.
*
* **Usage**
*
* ```js
* context.on('console', async msg => {
* const values = [];
* for (const arg of msg.args())
* values.push(await arg.jsonValue());
* console.log(...values);
* });
* await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ```
*
*/
on(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this;
/** /**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event
* will also fire for popup pages. See also * will also fire for popup pages. See also
@ -7527,6 +7548,11 @@ export interface BrowserContext {
*/ */
once(event: 'close', listener: (browserContext: BrowserContext) => void): this; once(event: 'close', listener: (browserContext: BrowserContext) => 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: 'console', listener: (consoleMessage: ConsoleMessage) => void): this;
/** /**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/ */
@ -7577,6 +7603,27 @@ export interface BrowserContext {
*/ */
addListener(event: 'close', listener: (browserContext: BrowserContext) => void): this; addListener(event: 'close', listener: (browserContext: BrowserContext) => void): this;
/**
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning.
*
* The arguments passed into `console.log` and the page are available on the [ConsoleMessage] event handler argument.
*
* **Usage**
*
* ```js
* context.on('console', async msg => {
* const values = [];
* for (const arg of msg.args())
* values.push(await arg.jsonValue());
* console.log(...values);
* });
* await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ```
*
*/
addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this;
/** /**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event
* will also fire for popup pages. See also * will also fire for popup pages. See also
@ -7655,6 +7702,11 @@ export interface BrowserContext {
*/ */
removeListener(event: 'close', listener: (browserContext: BrowserContext) => void): this; removeListener(event: 'close', listener: (browserContext: BrowserContext) => void): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this;
/** /**
* Removes an event listener added by `on` or `addListener`. * Removes an event listener added by `on` or `addListener`.
*/ */
@ -7695,6 +7747,11 @@ export interface BrowserContext {
*/ */
off(event: 'close', listener: (browserContext: BrowserContext) => void): this; off(event: 'close', listener: (browserContext: BrowserContext) => void): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this;
/** /**
* Removes an event listener added by `on` or `addListener`. * Removes an event listener added by `on` or `addListener`.
*/ */
@ -7745,6 +7802,27 @@ export interface BrowserContext {
*/ */
prependListener(event: 'close', listener: (browserContext: BrowserContext) => void): this; prependListener(event: 'close', listener: (browserContext: BrowserContext) => void): this;
/**
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning.
*
* The arguments passed into `console.log` and the page are available on the [ConsoleMessage] event handler argument.
*
* **Usage**
*
* ```js
* context.on('console', async msg => {
* const values = [];
* for (const arg of msg.args())
* values.push(await arg.jsonValue());
* console.log(...values);
* });
* await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ```
*
*/
prependListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this;
/** /**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event
* will also fire for popup pages. See also * will also fire for popup pages. See also
@ -8284,6 +8362,27 @@ export interface BrowserContext {
*/ */
waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (browserContext: BrowserContext) => boolean | Promise<boolean>, timeout?: number } | ((browserContext: BrowserContext) => boolean | Promise<boolean>)): Promise<BrowserContext>; waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (browserContext: BrowserContext) => boolean | Promise<boolean>, timeout?: number } | ((browserContext: BrowserContext) => boolean | Promise<boolean>)): Promise<BrowserContext>;
/**
* Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also
* emitted if the page throws an error or a warning.
*
* The arguments passed into `console.log` and the page are available on the [ConsoleMessage] event handler argument.
*
* **Usage**
*
* ```js
* context.on('console', async msg => {
* const values = [];
* for (const arg of msg.args())
* values.push(await arg.jsonValue());
* console.log(...values);
* });
* await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
* ```
*
*/
waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise<boolean>, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise<boolean>)): Promise<ConsoleMessage>;
/** /**
* The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event
* will also fire for popup pages. See also * will also fire for popup pages. See also
@ -16085,6 +16184,11 @@ export interface ConsoleMessage {
columnNumber: number; columnNumber: number;
}; };
/**
* The page that produced this console message, if any.
*/
page(): Page|null;
/** /**
* The text of the console message. * The text of the console message.
*/ */

View file

@ -1394,6 +1394,7 @@ export type BrowserContextInitializer = {
}; };
export interface BrowserContextEventTarget { export interface BrowserContextEventTarget {
on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this; on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this;
on(event: 'console', callback: (params: BrowserContextConsoleEvent) => void): this;
on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this; on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this;
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this;
@ -1435,6 +1436,9 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
export type BrowserContextBindingCallEvent = { export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel, binding: BindingCallChannel,
}; };
export type BrowserContextConsoleEvent = {
message: ConsoleMessageChannel,
};
export type BrowserContextCloseEvent = {}; export type BrowserContextCloseEvent = {};
export type BrowserContextPageEvent = { export type BrowserContextPageEvent = {
page: PageChannel, page: PageChannel,
@ -1677,6 +1681,7 @@ export type BrowserContextUpdateSubscriptionResult = void;
export interface BrowserContextEvents { export interface BrowserContextEvents {
'bindingCall': BrowserContextBindingCallEvent; 'bindingCall': BrowserContextBindingCallEvent;
'console': BrowserContextConsoleEvent;
'close': BrowserContextCloseEvent; 'close': BrowserContextCloseEvent;
'page': BrowserContextPageEvent; 'page': BrowserContextPageEvent;
'route': BrowserContextRouteEvent; 'route': BrowserContextRouteEvent;
@ -1702,7 +1707,6 @@ export type PageInitializer = {
export interface PageEventTarget { export interface PageEventTarget {
on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => void): this; on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => void): this;
on(event: 'close', callback: (params: PageCloseEvent) => void): this; on(event: 'close', callback: (params: PageCloseEvent) => void): this;
on(event: 'console', callback: (params: PageConsoleEvent) => void): this;
on(event: 'crash', callback: (params: PageCrashEvent) => void): this; on(event: 'crash', callback: (params: PageCrashEvent) => void): this;
on(event: 'dialog', callback: (params: PageDialogEvent) => void): this; on(event: 'dialog', callback: (params: PageDialogEvent) => void): this;
on(event: 'download', callback: (params: PageDownloadEvent) => void): this; on(event: 'download', callback: (params: PageDownloadEvent) => void): this;
@ -1755,9 +1759,6 @@ export type PageBindingCallEvent = {
binding: BindingCallChannel, binding: BindingCallChannel,
}; };
export type PageCloseEvent = {}; export type PageCloseEvent = {};
export type PageConsoleEvent = {
message: ConsoleMessageChannel,
};
export type PageCrashEvent = {}; export type PageCrashEvent = {};
export type PageDialogEvent = { export type PageDialogEvent = {
dialog: DialogChannel, dialog: DialogChannel,
@ -2201,7 +2202,6 @@ export type PageUpdateSubscriptionResult = void;
export interface PageEvents { export interface PageEvents {
'bindingCall': PageBindingCallEvent; 'bindingCall': PageBindingCallEvent;
'close': PageCloseEvent; 'close': PageCloseEvent;
'console': PageConsoleEvent;
'crash': PageCrashEvent; 'crash': PageCrashEvent;
'dialog': PageDialogEvent; 'dialog': PageDialogEvent;
'download': PageDownloadEvent; 'download': PageDownloadEvent;
@ -3687,6 +3687,7 @@ export interface WebSocketEvents {
// ----------- ConsoleMessage ----------- // ----------- ConsoleMessage -----------
export type ConsoleMessageInitializer = { export type ConsoleMessageInitializer = {
page: PageChannel,
type: string, type: string,
text: string, text: string,
args: JSHandleChannel[], args: JSHandleChannel[],

View file

@ -1157,6 +1157,10 @@ BrowserContext:
parameters: parameters:
binding: BindingCall binding: BindingCall
console:
parameters:
message: ConsoleMessage
close: close:
page: page:
@ -1573,10 +1577,6 @@ Page:
close: close:
console:
parameters:
message: ConsoleMessage
crash: crash:
dialog: dialog:
@ -2876,6 +2876,7 @@ ConsoleMessage:
type: interface type: interface
initializer: initializer:
page: Page
type: string type: string
text: string text: string
args: args:

View file

@ -0,0 +1,75 @@
/**
* Copyright (c) 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.
*/
import { browserTest as test, expect } from '../config/browserTest';
test('console event should work @smoke', async ({ page }) => {
const [, message] = await Promise.all([
page.evaluate(() => console.log('hello')),
page.context().waitForEvent('console'),
]);
expect(message.text()).toBe('hello');
expect(message.page()).toBe(page);
});
test('console event should work in popup', async ({ page }) => {
const [, message, popup] = await Promise.all([
page.evaluate(() => {
const win = window.open('');
(win as any).console.log('hello');
}),
page.context().waitForEvent('console'),
page.waitForEvent('popup'),
]);
expect(message.text()).toBe('hello');
expect(message.page()).toBe(popup);
});
test('console event should work in popup 2', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'console message from javascript: url is not reported at all');
const [, message, popup] = await Promise.all([
page.evaluate(async () => {
const win = window.open('javascript:console.log("hello")');
await new Promise(f => setTimeout(f, 0));
win.close();
}),
page.context().waitForEvent('console', msg => msg.type() === 'log'),
page.context().waitForEvent('page'),
]);
expect(message.text()).toBe('hello');
expect(message.page()).toBe(popup);
});
test('console event should work in immediately closed popup', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'console message is not reported at all');
const [, message, popup] = await Promise.all([
page.evaluate(async () => {
const win = window.open();
(win as any).console.log('hello');
win.close();
}),
page.context().waitForEvent('console'),
page.waitForEvent('popup'),
]);
expect(message.text()).toBe('hello');
expect(message.page()).toBe(popup);
});

View file

@ -122,7 +122,7 @@ test('should contain action info', async ({ showTraceViewer }) => {
test('should render events', async ({ showTraceViewer }) => { test('should render events', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
const events = await traceViewer.eventBars(); const events = await traceViewer.eventBars();
expect(events).toContain('page_console'); expect(events).toContain('browsercontext_console');
}); });
test('should render console', async ({ showTraceViewer, browserName }) => { test('should render console', async ({ showTraceViewer, browserName }) => {