diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index f3bef29d01..fc798f1224 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -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()); +}; + +await page.EvaluateAsync("console.log('hello', 5, { foo: 'bar' })"); +``` + ## event: BrowserContext.page * since: v1.8 - argument: <[Page]> diff --git a/docs/src/api/class-consolemessage.md b/docs/src/api/class-consolemessage.md index 9fb1a9424c..5d1a3d0ff2 100644 --- a/docs/src/api/class-consolemessage.md +++ b/docs/src/api/class-consolemessage.md @@ -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]> diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index ef304b556f..0b5dac85e2 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -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 => { @@ -177,7 +176,7 @@ 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 @@ -185,7 +184,7 @@ 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 @@ -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 @@ -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 diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 9851382570..0b94b9339a 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -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 implements api.BrowserContext { _pages = new Set(); @@ -92,6 +93,13 @@ export class BrowserContext extends ChannelOwner 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)); diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index df11c23672..11f12babd4 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -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']; @@ -27,8 +28,18 @@ export class ConsoleMessage extends ChannelOwner 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 { diff --git a/packages/playwright-core/src/client/events.ts b/packages/playwright-core/src/client/events.ts index 1d2cf9f52b..55d8848c28 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -35,6 +35,7 @@ export const Events = { }, BrowserContext: { + Console: 'console', Close: 'close', Page: 'page', BackgroundPage: 'backgroundpage', diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 28c9b27ecb..db24693072 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -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'; @@ -124,7 +123,6 @@ export class Page extends ChannelOwner 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); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5a660157d1..afc37f4d26 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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']), @@ -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']), @@ -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'])), diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index e339c308f8..910d729811 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -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', diff --git a/packages/playwright-core/src/server/console.ts b/packages/playwright-core/src/server/console.ts index ccb23e7b39..5b0de45a0f 100644 --- a/packages/playwright-core/src/server/console.ts +++ b/packages/playwright-core/src/server/console.ts @@ -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; } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 09e04074db..4f815c560b 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -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 implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -79,6 +80,7 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this, message) })); if (context._browser.options.name === 'chromium') { for (const page of (context as CRBrowserContext).backgroundPages()) diff --git a/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts index ae1d2124b9..c8228811da 100644 --- a/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts @@ -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 implements channels.ConsoleMessageChannel { +export class ConsoleMessageDispatcher extends Dispatcher 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, }); } } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index b14b897c47..290a176a4f 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -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'; @@ -76,7 +75,6 @@ export class PageDispatcher extends Dispatcher 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) => { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index ed92d79bde..43a0e1200c 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -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(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(); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 6f6725d24f..59269f17de 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -121,7 +121,6 @@ export class Page extends SdkObject { static Events = { Close: 'close', Crash: 'crash', - Console: 'console', Dialog: 'dialog', Download: 'download', FileChooser: 'filechooser', @@ -141,6 +140,7 @@ export class Page extends SdkObject { private _closedPromise = new ManualPromise(); private _disconnected = false; private _initialized = false; + private _consoleMessagesBeforeInitialized: ConsoleMessage[] = []; readonly _disconnectedPromise = new ManualPromise(); readonly _crashedPromise = new ManualPromise(); readonly _browserContext: BrowserContext; @@ -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() { @@ -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 { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 676fd439e2..efdf3c5ab2 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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 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 => { @@ -889,7 +889,7 @@ export interface Page { * 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' })); * ``` * */ @@ -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 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 => { @@ -1182,7 +1182,7 @@ export interface Page { * 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' })); * ``` * */ @@ -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 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 => { @@ -1570,7 +1570,7 @@ export interface Page { * 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' })); * ``` * */ @@ -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 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 => { @@ -4208,7 +4208,7 @@ export interface Page { * 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' })); * ``` * */ @@ -7449,6 +7449,27 @@ export interface BrowserContext { */ 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 * will also fire for popup pages. See also @@ -7527,6 +7548,11 @@ export interface BrowserContext { */ 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. */ @@ -7577,6 +7603,27 @@ export interface BrowserContext { */ 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 * will also fire for popup pages. See also @@ -7655,6 +7702,11 @@ export interface BrowserContext { */ 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`. */ @@ -7695,6 +7747,11 @@ export interface BrowserContext { */ 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`. */ @@ -7745,6 +7802,27 @@ export interface BrowserContext { */ 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 * will also fire for popup pages. See also @@ -8284,6 +8362,27 @@ export interface BrowserContext { */ waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (browserContext: BrowserContext) => boolean | Promise, timeout?: number } | ((browserContext: BrowserContext) => boolean | Promise)): Promise; + /** + * 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, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; + /** * 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 @@ -16085,6 +16184,11 @@ export interface ConsoleMessage { columnNumber: number; }; + /** + * The page that produced this console message, if any. + */ + page(): Page|null; + /** * The text of the console message. */ diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 85ce51aaeb..bcc99f0a35 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1394,6 +1394,7 @@ export type BrowserContextInitializer = { }; export interface BrowserContextEventTarget { 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: 'page', callback: (params: BrowserContextPageEvent) => void): this; on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; @@ -1435,6 +1436,9 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, }; +export type BrowserContextConsoleEvent = { + message: ConsoleMessageChannel, +}; export type BrowserContextCloseEvent = {}; export type BrowserContextPageEvent = { page: PageChannel, @@ -1677,6 +1681,7 @@ export type BrowserContextUpdateSubscriptionResult = void; export interface BrowserContextEvents { 'bindingCall': BrowserContextBindingCallEvent; + 'console': BrowserContextConsoleEvent; 'close': BrowserContextCloseEvent; 'page': BrowserContextPageEvent; 'route': BrowserContextRouteEvent; @@ -1702,7 +1707,6 @@ export type PageInitializer = { export interface PageEventTarget { on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => 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: 'dialog', callback: (params: PageDialogEvent) => void): this; on(event: 'download', callback: (params: PageDownloadEvent) => void): this; @@ -1755,9 +1759,6 @@ export type PageBindingCallEvent = { binding: BindingCallChannel, }; export type PageCloseEvent = {}; -export type PageConsoleEvent = { - message: ConsoleMessageChannel, -}; export type PageCrashEvent = {}; export type PageDialogEvent = { dialog: DialogChannel, @@ -2201,7 +2202,6 @@ export type PageUpdateSubscriptionResult = void; export interface PageEvents { 'bindingCall': PageBindingCallEvent; 'close': PageCloseEvent; - 'console': PageConsoleEvent; 'crash': PageCrashEvent; 'dialog': PageDialogEvent; 'download': PageDownloadEvent; @@ -3687,6 +3687,7 @@ export interface WebSocketEvents { // ----------- ConsoleMessage ----------- export type ConsoleMessageInitializer = { + page: PageChannel, type: string, text: string, args: JSHandleChannel[], diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 380196a742..bbefbbded6 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1157,6 +1157,10 @@ BrowserContext: parameters: binding: BindingCall + console: + parameters: + message: ConsoleMessage + close: page: @@ -1573,10 +1577,6 @@ Page: close: - console: - parameters: - message: ConsoleMessage - crash: dialog: @@ -2876,6 +2876,7 @@ ConsoleMessage: type: interface initializer: + page: Page type: string text: string args: diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts new file mode 100644 index 0000000000..51b30688cb --- /dev/null +++ b/tests/library/browsercontext-events.spec.ts @@ -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); +}); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index faa3cb5f4b..6950fda119 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -122,7 +122,7 @@ test('should contain action info', async ({ showTraceViewer }) => { test('should render events', async ({ showTraceViewer }) => { const traceViewer = await showTraceViewer([traceFile]); const events = await traceViewer.eventBars(); - expect(events).toContain('page_console'); + expect(events).toContain('browsercontext_console'); }); test('should render console', async ({ showTraceViewer, browserName }) => {