Skip to content

Commit

Permalink
feat: browserContext.on('console') (#21943)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Mar 27, 2023
1 parent 525097d commit f502c72
Show file tree
Hide file tree
Showing 20 changed files with 340 additions and 50 deletions.
60 changes: 60 additions & 0 deletions docs/src/api/class-browsercontext.md
Expand Up @@ -94,6 +94,66 @@ Emitted when Browser context gets closed. This might happen because of one of th
* Browser application is closed or crashed.
* 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
* since: v1.8
- argument: <[Page]>
Expand Down
6 changes: 6 additions & 0 deletions docs/src/api/class-consolemessage.md
Expand Up @@ -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`.

## method: ConsoleMessage.page
* since: v1.33
- returns: <[Page]|[null]>

The page that produced this console message, if any.

## method: ConsoleMessage.text
* since: v1.8
- returns: <[string]>
Expand Down
15 changes: 7 additions & 8 deletions docs/src/api/class-page.md
Expand Up @@ -163,12 +163,11 @@ Emitted when the page closes.
- 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.
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` 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
page.on('console', async msg => {
Expand All @@ -177,15 +176,15 @@ page.on('console', async msg => {
values.push(await arg.jsonValue());
console.log(...values);
});
await page.evaluate(() => console.log('hello', 5, {foo: 'bar'}));
await page.evaluate(() => console.log('hello', 5, { foo: 'bar' }));
```

```java
page.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'})");
page.evaluate("() => console.log('hello', 5, { foo: 'bar' })");
```

```python async
Expand All @@ -196,7 +195,7 @@ async def print_args(msg):
print(values)

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
Expand All @@ -205,7 +204,7 @@ def print_args(msg):
print(arg.json_value())

page.on("console", print_args)
page.evaluate("console.log('hello', 5, {foo: 'bar'})")
page.evaluate("console.log('hello', 5, { foo: 'bar' })")
```

```csharp
Expand Down
8 changes: 8 additions & 0 deletions packages/playwright-core/src/client/browserContext.ts
Expand Up @@ -40,6 +40,7 @@ import { APIRequestContext } from './fetch';
import { createInstrumentation } from './clientInstrumentation';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { HarRouter } from './harRouter';
import { ConsoleMessage } from './consoleMessage';

export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
Expand Down Expand Up @@ -92,6 +93,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._serviceWorkers.add(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('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
this._channel.on('requestFinished', params => this._onRequestFinished(params));
Expand Down
11 changes: 11 additions & 0 deletions packages/playwright-core/src/client/consoleMessage.ts
Expand Up @@ -19,6 +19,7 @@ import { JSHandle } from './jsHandle';
import type * as channels from '@protocol/channels';
import { ChannelOwner } from './channelOwner';
import type * as api from '../../types/types';
import { Page } from './page';

type ConsoleMessageLocation = channels.ConsoleMessageInitializer['location'];

Expand All @@ -27,8 +28,18 @@ export class ConsoleMessage extends ChannelOwner<channels.ConsoleMessageChannel>
return (message as any)._object;
}

private _page: Page | null;

constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ConsoleMessageInitializer) {
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 {
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/client/events.ts
Expand Up @@ -35,6 +35,7 @@ export const Events = {
},

BrowserContext: {
Console: 'console',
Close: 'close',
Page: 'page',
BackgroundPage: 'backgroundpage',
Expand Down
2 changes: 0 additions & 2 deletions packages/playwright-core/src/client/page.ts
Expand Up @@ -31,7 +31,6 @@ import { Artifact } from './artifact';
import type { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { evaluationScript } from './clientHelper';
import { ConsoleMessage } from './consoleMessage';
import { Coverage } from './coverage';
import { Dialog } from './dialog';
import { Download } from './download';
Expand Down Expand Up @@ -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('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('dialog', ({ dialog }) => {
const dialogObj = Dialog.from(dialog);
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright-core/src/protocol/validator.ts
Expand Up @@ -755,6 +755,9 @@ scheme.BrowserContextInitializer = tObject({
scheme.BrowserContextBindingCallEvent = tObject({
binding: tChannel(['BindingCall']),
});
scheme.BrowserContextConsoleEvent = tObject({
message: tChannel(['ConsoleMessage']),
});
scheme.BrowserContextCloseEvent = tOptional(tObject({}));
scheme.BrowserContextPageEvent = tObject({
page: tChannel(['Page']),
Expand Down Expand Up @@ -929,9 +932,6 @@ scheme.PageBindingCallEvent = tObject({
binding: tChannel(['BindingCall']),
});
scheme.PageCloseEvent = tOptional(tObject({}));
scheme.PageConsoleEvent = tObject({
message: tChannel(['ConsoleMessage']),
});
scheme.PageCrashEvent = tOptional(tObject({}));
scheme.PageDialogEvent = tObject({
dialog: tChannel(['Dialog']),
Expand Down Expand Up @@ -2072,6 +2072,7 @@ scheme.WebSocketSocketErrorEvent = tObject({
});
scheme.WebSocketCloseEvent = tOptional(tObject({}));
scheme.ConsoleMessageInitializer = tObject({
page: tChannel(['Page']),
type: tString,
text: tString,
args: tArray(tChannel(['ElementHandle', 'JSHandle'])),
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/server/browserContext.ts
Expand Up @@ -44,6 +44,7 @@ import type { Artifact } from './artifact';

export abstract class BrowserContext extends SdkObject {
static Events = {
Console: 'console',
Close: 'close',
Page: 'page',
Request: 'request',
Expand Down
11 changes: 9 additions & 2 deletions packages/playwright-core/src/server/console.ts
Expand Up @@ -17,21 +17,28 @@
import { SdkObject } from './instrumentation';
import type * as js from './javascript';
import type { ConsoleMessageLocation } from './types';
import type { Page } from './page';

export class ConsoleMessage extends SdkObject {
private _type: string;
private _text?: string;
private _args: js.JSHandle[];
private _location: ConsoleMessageLocation;
private _page: Page;

constructor(parent: SdkObject, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) {
super(parent, 'console-message');
constructor(page: Page, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) {
super(page, 'console-message');
this._page = page;
this._type = type;
this._text = text;
this._args = args;
this._location = location || { url: '', lineNumber: 0, columnNumber: 0 };
}

page() {
return this._page;
}

type(): string {
return this._type;
}
Expand Down
Expand Up @@ -33,6 +33,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { createGuid, urlMatches } from '../../utils';
import { WritableStreamDispatcher } from './writableStreamDispatcher';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';

export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true;
Expand Down Expand Up @@ -79,6 +80,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('close');
this._dispose();
});
this.addObjectListener(BrowserContext.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this, message) }));

if (context._browser.options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages())
Expand Down
Expand Up @@ -16,19 +16,22 @@

import type { ConsoleMessage } from '../console';
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 { 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;

constructor(scope: PageDispatcher, message: ConsoleMessage) {
constructor(scope: BrowserContextDispatcher, message: ConsoleMessage) {
const page = PageDispatcher.from(scope, message.page());
super(scope, message, 'ConsoleMessage', {
type: message.type(),
text: message.text(),
args: message.args().map(a => ElementHandleDispatcher.fromJSHandle(scope, a)),
args: message.args().map(a => ElementHandleDispatcher.fromJSHandle(page, a)),
location: message.location(),
page,
});
}
}
Expand Up @@ -20,7 +20,6 @@ import { Page, Worker } from '../page';
import type * as channels from '@protocol/channels';
import { Dispatcher, existingDispatcher } from './dispatcher';
import { parseError, serializeError } from '../../protocol/serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { RequestDispatcher } from './networkDispatchers';
Expand Down Expand Up @@ -76,7 +75,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
this._dispatchEvent('close');
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.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) }));
this.addObjectListener(Page.Events.Download, (download: Download) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/frames.ts
Expand Up @@ -1028,8 +1028,8 @@ export class Frame extends SdkObject {
let cspMessage: ConsoleMessage | undefined;
const actionPromise = func().then(r => result = r).catch(e => error = e);
const errorPromise = new Promise<void>(resolve => {
listeners.push(eventsHelper.addEventListener(this._page, Page.Events.Console, (message: ConsoleMessage) => {
if (message.type() === 'error' && message.text().includes('Content Security Policy')) {
listeners.push(eventsHelper.addEventListener(this._page._browserContext, BrowserContext.Events.Console, (message: ConsoleMessage) => {
if (message.page() === this._page && message.type() === 'error' && message.text().includes('Content Security Policy')) {
cspMessage = message;
resolve();
}
Expand Down
24 changes: 19 additions & 5 deletions packages/playwright-core/src/server/page.ts
Expand Up @@ -121,7 +121,6 @@ export class Page extends SdkObject {
static Events = {
Close: 'close',
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
Download: 'download',
FileChooser: 'filechooser',
Expand All @@ -141,6 +140,7 @@ export class Page extends SdkObject {
private _closedPromise = new ManualPromise<void>();
private _disconnected = false;
private _initialized = false;
private _consoleMessagesBeforeInitialized: ConsoleMessage[] = [];
readonly _disconnectedPromise = new ManualPromise<Error>();
readonly _crashedPromise = new ManualPromise<Error>();
readonly _browserContext: BrowserContext;
Expand Down Expand Up @@ -208,12 +208,18 @@ export class Page extends SdkObject {
}
this._initialized = true;
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
// corresponding Close event after it is reported on the context.
if (this.isClosed())
this.emit(Page.Events.Close);
this.instrumentation.onPageOpen(this);
else
this.instrumentation.onPageOpen(this);
}

initializedOrUndefined() {
Expand Down Expand Up @@ -351,10 +357,18 @@ export class Page extends SdkObject {
_addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) {
const message = new ConsoleMessage(this, type, text, args, location);
const intercepted = this._frameManager.interceptConsoleMessage(message);
if (intercepted || !this.listenerCount(Page.Events.Console))
if (intercepted) {
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
this.emit(Page.Events.Console, message);
this._consoleMessagesBeforeInitialized.push(message);
}

async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise<network.Response | null> {
Expand Down

0 comments on commit f502c72

Please sign in to comment.