diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index f3bef29d01..3ae900ff37 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -94,6 +94,102 @@ 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.dialog +* since: v1.33 +- argument: <[Dialog]> + +Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** either [`method: Dialog.accept`] or [`method: Dialog.dismiss`] the dialog - otherwise the page will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and actions like click will never finish. + +**Usage** + +```js +context.on('dialog', dialog => { + dialog.accept(); +}); +``` + +```java +context.onDialog(dialog -> { + dialog.accept(); +}); +``` + +```python +context.on("dialog", lambda dialog: dialog.accept()) +``` + +```csharp +context.RequestFailed += (_, request) => +{ + Console.WriteLine(request.Url + " " + request.Failure); +}; +``` + +:::note +When no [`event: Page.dialog`] or [`event: BrowserContext.dialog`] listeners are present, all dialogs are automatically dismissed. +::: + ## 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 1a230b24e3..925014cc72 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-dialog.md b/docs/src/api/class-dialog.md index b013f4b69c..37935d0d9d 100644 --- a/docs/src/api/class-dialog.md +++ b/docs/src/api/class-dialog.md @@ -137,6 +137,12 @@ Returns when the dialog has been dismissed. A message displayed in the dialog. +## method: Dialog.page +* since: v1.33 +- returns: <[Page]|[null]> + +The page that initiated this dialog, if available. + ## method: Dialog.type * since: v1.8 - returns: <[string]> diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 523daa725e..cffd8be27a 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 @@ -286,6 +285,8 @@ try { Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** either [`method: Dialog.accept`] or [`method: Dialog.dismiss`] the dialog - otherwise the page will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and actions like click will never finish. +**Usage** + ```js page.on('dialog', dialog => { dialog.accept(); @@ -310,7 +311,7 @@ page.RequestFailed += (_, request) => ``` :::note -When no [`event: Page.dialog`] listeners are present, all dialogs are automatically dismissed. +When no [`event: Page.dialog`] or [`event: BrowserContext.dialog`] listeners are present, all dialogs are automatically dismissed. ::: ## event: Page.DOMContentLoaded diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 67e57944f8..7ca870305c 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -39,6 +39,8 @@ import { Artifact } from './artifact'; import { APIRequestContext } from './fetch'; import { rewriteErrorMessage } from '../utils/stackTrace'; import { HarRouter } from './harRouter'; +import { ConsoleMessage } from './consoleMessage'; +import { Dialog } from './dialog'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); @@ -91,6 +93,26 @@ 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('dialog', ({ dialog }) => { + const dialogObject = Dialog.from(dialog); + let hasListeners = this.emit(Events.BrowserContext.Dialog, dialogObject); + const page = dialogObject.page(); + if (page) + hasListeners = page.emit(Events.Page.Dialog, dialogObject) || hasListeners; + if (!hasListeners) { + if (dialogObject.type() === 'beforeunload') + dialog.accept({}).catch(() => {}); + else + dialog.dismiss().catch(() => {}); + } + }); 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/dialog.ts b/packages/playwright-core/src/client/dialog.ts index e302e8d8d1..c012838b7c 100644 --- a/packages/playwright-core/src/client/dialog.ts +++ b/packages/playwright-core/src/client/dialog.ts @@ -17,14 +17,24 @@ import type * as channels from '@protocol/channels'; import { ChannelOwner } from './channelOwner'; import type * as api from '../../types/types'; +import { Page } from './page'; export class Dialog extends ChannelOwner implements api.Dialog { static from(dialog: channels.DialogChannel): Dialog { return (dialog as any)._object; } + private _page: Page | null; + constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.DialogInitializer) { super(parent, type, guid, initializer); + // Note: dialogs that open early during page initialization block it. + // Therefore, we must report the dialog without a page to be able to handle it. + 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..ceca0829d6 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -35,7 +35,9 @@ export const Events = { }, BrowserContext: { + Console: 'console', Close: 'close', + Dialog: 'dialog', Page: 'page', BackgroundPage: 'backgroundpage', ServiceWorker: 'serviceworker', diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index b9b1d9768b..3453bc33ec 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -31,9 +31,7 @@ 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'; import { determineScreenshotType, ElementHandle } from './elementHandle'; import { Events } from './events'; @@ -124,17 +122,7 @@ 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); - if (!this.emit(Events.Page.Dialog, dialogObj)) { - if (dialogObj.type() === 'beforeunload') - dialog.accept({}).catch(() => {}); - else - dialog.dismiss().catch(() => {}); - } - }); this._channel.on('download', ({ url, suggestedFilename, artifact }) => { const artifactObject = Artifact.from(artifact); this.emit(Events.Page.Download, new Download(this, url, suggestedFilename, artifactObject)); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 8a16efc7d1..f0a0cdb411 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -760,7 +760,13 @@ scheme.BrowserContextInitializer = tObject({ scheme.BrowserContextBindingCallEvent = tObject({ binding: tChannel(['BindingCall']), }); +scheme.BrowserContextConsoleEvent = tObject({ + message: tChannel(['ConsoleMessage']), +}); scheme.BrowserContextCloseEvent = tOptional(tObject({})); +scheme.BrowserContextDialogEvent = tObject({ + dialog: tChannel(['Dialog']), +}); scheme.BrowserContextPageEvent = tObject({ page: tChannel(['Page']), }); @@ -935,13 +941,7 @@ 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']), -}); scheme.PageDownloadEvent = tObject({ url: tString, suggestedFilename: tString, @@ -2084,6 +2084,7 @@ scheme.WebSocketSocketErrorEvent = tObject({ }); scheme.WebSocketCloseEvent = tOptional(tObject({})); scheme.ConsoleMessageInitializer = tObject({ + page: tChannel(['Page']), type: tString, text: tString, args: tArray(tChannel(['ElementHandle', 'JSHandle'])), @@ -2108,6 +2109,7 @@ scheme.BindingCallResolveParams = tObject({ }); scheme.BindingCallResolveResult = tOptional(tObject({})); scheme.DialogInitializer = tObject({ + page: tOptional(tChannel(['Page'])), type: tString, message: tString, defaultValue: tString, diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index e339c308f8..c92b671304 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -44,7 +44,9 @@ import type { Artifact } from './artifact'; export abstract class BrowserContext extends SdkObject { static Events = { + Console: 'console', Close: 'close', + Dialog: 'dialog', Page: 'page', Request: 'request', Response: 'response', diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index e66868b2cd..080a835ca6 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -845,7 +845,7 @@ class FrameSession { _onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) { if (!this._page._frameManager.frame(this._targetId)) return; // Our frame/subtree may be gone already. - this._page.emit(Page.Events.Dialog, new dialog.Dialog( + this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog( this._page, event.type, event.message, 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/dialog.ts b/packages/playwright-core/src/server/dialog.ts index dd1683e9aa..51dcfc2fc9 100644 --- a/packages/playwright-core/src/server/dialog.ts +++ b/packages/playwright-core/src/server/dialog.ts @@ -41,6 +41,10 @@ export class Dialog extends SdkObject { this._page._frameManager.dialogDidOpen(this); } + 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..5ac9649588 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -33,6 +33,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { createGuid, urlMatches } from '../../utils'; import { WritableStreamDispatcher } from './writableStreamDispatcher'; +import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; +import { DialogDispatcher } from './dialogDispatcher'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -79,6 +81,8 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(PageDispatcher.from(this, message.page()), message) })); + this.addObjectListener(BrowserContext.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) })); 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..35354a5ed5 100644 --- a/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts @@ -23,12 +23,13 @@ import { ElementHandleDispatcher } from './elementHandlerDispatcher'; export class ConsoleMessageDispatcher extends Dispatcher implements channels.ConsoleMessageChannel { _type_ConsoleMessage = true; - constructor(scope: PageDispatcher, message: ConsoleMessage) { - super(scope, message, 'ConsoleMessage', { + constructor(page: PageDispatcher, message: ConsoleMessage) { + super(page, 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/dialogDispatcher.ts b/packages/playwright-core/src/server/dispatchers/dialogDispatcher.ts index 8ae74504f8..50ff01f108 100644 --- a/packages/playwright-core/src/server/dispatchers/dialogDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dialogDispatcher.ts @@ -17,13 +17,17 @@ import type { Dialog } from '../dialog'; import type * as channels from '@protocol/channels'; import { Dispatcher } from './dispatcher'; -import type { PageDispatcher } from './pageDispatcher'; +import { PageDispatcher } from './pageDispatcher'; +import type { BrowserContextDispatcher } from './browserContextDispatcher'; -export class DialogDispatcher extends Dispatcher implements channels.DialogChannel { +export class DialogDispatcher extends Dispatcher implements channels.DialogChannel { _type_Dialog = true; - constructor(scope: PageDispatcher, dialog: Dialog) { - super(scope, dialog, 'Dialog', { + constructor(scope: BrowserContextDispatcher, dialog: Dialog) { + const page = PageDispatcher.fromNullable(scope, dialog.page().initializedOrUndefined()); + // Prefer scoping to the page, unless we don't have one. + super(page || scope, dialog, 'Dialog', { + page, type: dialog.type(), message: dialog.message(), defaultValue: dialog.defaultValue(), diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index b14b897c47..3e249692e2 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -20,8 +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'; import { ResponseDispatcher } from './networkDispatchers'; @@ -76,9 +74,7 @@ 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) => { // Artifact can outlive the page, so bind to the context scope. this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: ArtifactDispatcher.from(parentScope, download.artifact) }); diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 47be1684d6..d3db91f9e1 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -34,6 +34,7 @@ import type { Progress } from '../progress'; import { splitErrorMessage } from '../../utils/stackTrace'; import { debugLogger } from '../../common/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; +import { BrowserContext } from '../browserContext'; export const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -257,7 +258,7 @@ export class FFPage implements PageDelegate { } _onDialogOpened(params: Protocol.Page.dialogOpenedPayload) { - this._page.emit(Page.Events.Dialog, new dialog.Dialog( + this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog( this._page, params.type, params.message, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 7cde76438f..950b844ce2 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1036,8 +1036,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 ff4d711a56..8bdbb58d60 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -121,8 +121,6 @@ export class Page extends SdkObject { static Events = { Close: 'close', Crash: 'crash', - Console: 'console', - Dialog: 'dialog', Download: 'download', FileChooser: 'filechooser', // Can't use just 'error' due to node.js special treatment of error events. @@ -141,6 +139,7 @@ export class Page extends SdkObject { private _closedPromise = new ManualPromise(); private _disconnected = false; private _initialized = false; + private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = []; readonly _disconnectedPromise = new ManualPromise(); readonly _crashedPromise = new ManualPromise(); readonly _browserContext: BrowserContext; @@ -208,12 +207,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 { event, args } of this._eventsToEmitAfterInitialized) + this._browserContext.emit(event, ...args); + this._eventsToEmitAfterInitialized = []; + + // 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() { @@ -226,6 +231,19 @@ export class Page extends SdkObject { this._browserContext.emit(event, ...args); } + emitOnContextOnceInitialized(event: string | symbol, ...args: any[]) { + if (this._isServerSideOnly) + return; + // Some events, like console messages, may come before page is ready. + // In this case, postpone the event 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._browserContext.emit(event, ...args); + else + this._eventsToEmitAfterInitialized.push({ event, args }); + } + async resetForReuse(metadata: CallMetadata) { this.setDefaultNavigationTimeout(undefined); this.setDefaultTimeout(undefined); @@ -351,10 +369,11 @@ 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()); - else - this.emit(Page.Events.Console, message); + return; + } + this.emitOnContextOnceInitialized(BrowserContext.Events.Console, message); } async reload(metadata: CallMetadata, options: types.NavigateOptions): Promise { diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 8aa65a5922..cc9227241a 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -43,6 +43,7 @@ import { raceAgainstTimeout } from '../utils/timeoutRunner'; import type { Language, LanguageGenerator } from './recorder/language'; import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; import { eventsHelper, type RegisteredListener } from './../utils/eventsHelper'; +import type { Dialog } from './dialog'; type BindingSource = { frame: Frame, page: Page }; @@ -424,9 +425,10 @@ class ContextRecorder extends EventEmitter { } async install() { - this._context.on(BrowserContext.Events.Page, page => this._onPage(page)); + this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page)); for (const page of this._context.pages()) this._onPage(page); + this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page())); // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. @@ -470,7 +472,6 @@ class ContextRecorder extends EventEmitter { this._onFrameNavigated(frame, page); }); page.on(Page.Events.Download, () => this._onDownload(page)); - page.on(Page.Events.Dialog, () => this._onDialog(page)); const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; const pageAlias = 'page' + suffix; this._pageAliases.set(page, pageAlias); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 1a336841ae..99dc45e6af 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -44,6 +44,7 @@ import { WKProvisionalPage } from './wkProvisionalPage'; import { WKWorkers } from './wkWorkers'; import { debugLogger } from '../../common/debugLogger'; import { ManualPromise } from '../../utils/manualPromise'; +import { BrowserContext } from '../browserContext'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -607,7 +608,7 @@ export class WKPage implements PageDelegate { } _onDialog(event: Protocol.Dialog.javascriptDialogOpeningPayload) { - this._page.emit(Page.Events.Dialog, new dialog.Dialog( + this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog( this._page, event.type as dialog.DialogType, event.message, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index fb56584b58..9d6b9f1724 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -894,9 +894,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 => { @@ -905,7 +905,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' })); * ``` * */ @@ -938,14 +938,17 @@ export interface Page { * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the * dialog, and actions like click will never finish. * + * **Usage** + * * ```js * page.on('dialog', dialog => { * dialog.accept(); * }); * ``` * - * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) listeners are - * present, all dialogs are automatically dismissed. + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. */ on(event: 'dialog', listener: (dialog: Dialog) => void): this; @@ -1187,9 +1190,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 => { @@ -1198,7 +1201,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' })); * ``` * */ @@ -1231,14 +1234,17 @@ export interface Page { * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the * dialog, and actions like click will never finish. * + * **Usage** + * * ```js * page.on('dialog', dialog => { * dialog.accept(); * }); * ``` * - * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) listeners are - * present, all dialogs are automatically dismissed. + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. */ addListener(event: 'dialog', listener: (dialog: Dialog) => void): this; @@ -1575,9 +1581,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 => { @@ -1586,7 +1592,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' })); * ``` * */ @@ -1619,14 +1625,17 @@ export interface Page { * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the * dialog, and actions like click will never finish. * + * **Usage** + * * ```js * page.on('dialog', dialog => { * dialog.accept(); * }); * ``` * - * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) listeners are - * present, all dialogs are automatically dismissed. + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. */ prependListener(event: 'dialog', listener: (dialog: Dialog) => void): this; @@ -4227,9 +4236,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 => { @@ -4238,7 +4247,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' })); * ``` * */ @@ -4271,14 +4280,17 @@ export interface Page { * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the * dialog, and actions like click will never finish. * + * **Usage** + * * ```js * page.on('dialog', dialog => { * dialog.accept(); * }); * ``` * - * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) listeners are - * present, all dialogs are automatically dismissed. + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. */ waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise, timeout?: number } | ((dialog: Dialog) => boolean | Promise)): Promise; @@ -7514,6 +7526,48 @@ 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; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + * either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept) or + * [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss) the dialog - otherwise the page + * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the + * dialog, and actions like click will never finish. + * + * **Usage** + * + * ```js + * context.on('dialog', dialog => { + * dialog.accept(); + * }); + * ``` + * + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. + */ + on(event: 'dialog', listener: (dialog: Dialog) => 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 @@ -7592,6 +7646,16 @@ 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. + */ + once(event: 'dialog', listener: (dialog: Dialog) => void): this; + /** * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. */ @@ -7642,6 +7706,48 @@ 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; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + * either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept) or + * [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss) the dialog - otherwise the page + * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the + * dialog, and actions like click will never finish. + * + * **Usage** + * + * ```js + * context.on('dialog', dialog => { + * dialog.accept(); + * }); + * ``` + * + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. + */ + addListener(event: 'dialog', listener: (dialog: Dialog) => 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 @@ -7720,6 +7826,16 @@ 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`. + */ + removeListener(event: 'dialog', listener: (dialog: Dialog) => void): this; + /** * Removes an event listener added by `on` or `addListener`. */ @@ -7760,6 +7876,16 @@ 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`. + */ + off(event: 'dialog', listener: (dialog: Dialog) => void): this; + /** * Removes an event listener added by `on` or `addListener`. */ @@ -7810,6 +7936,48 @@ 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; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + * either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept) or + * [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss) the dialog - otherwise the page + * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the + * dialog, and actions like click will never finish. + * + * **Usage** + * + * ```js + * context.on('dialog', dialog => { + * dialog.accept(); + * }); + * ``` + * + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. + */ + prependListener(event: 'dialog', listener: (dialog: Dialog) => 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 @@ -8349,6 +8517,48 @@ 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; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** + * either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept) or + * [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss) the dialog - otherwise the page + * will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the + * dialog, and actions like click will never finish. + * + * **Usage** + * + * ```js + * context.on('dialog', dialog => { + * dialog.accept(); + * }); + * ``` + * + * **NOTE** When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#page-event-dialog) or + * [browserContext.on('dialog')](https://playwright.dev/docs/api/class-browsercontext#browser-context-event-dialog) + * listeners are present, all dialogs are automatically dismissed. + */ + waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise, timeout?: number } | ((dialog: Dialog) => 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 @@ -16196,6 +16406,11 @@ export interface ConsoleMessage { columnNumber: number; }; + /** + * The page that produced this console message, if any. + */ + page(): Page|null; + /** * The text of the console message. */ @@ -16393,6 +16608,11 @@ export interface Dialog { */ message(): string; + /** + * The page that initiated this dialog, if available. + */ + page(): Page|null; + /** * Returns dialog's type, can be one of `alert`, `beforeunload`, `confirm` or `prompt`. */ diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index c0966dfa0b..03311c4f6f 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1401,7 +1401,9 @@ 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: 'dialog', callback: (params: BrowserContextDialogEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; on(event: 'video', callback: (params: BrowserContextVideoEvent) => void): this; @@ -1442,7 +1444,13 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, }; +export type BrowserContextConsoleEvent = { + message: ConsoleMessageChannel, +}; export type BrowserContextCloseEvent = {}; +export type BrowserContextDialogEvent = { + dialog: DialogChannel, +}; export type BrowserContextPageEvent = { page: PageChannel, }; @@ -1686,7 +1694,9 @@ export type BrowserContextUpdateSubscriptionResult = void; export interface BrowserContextEvents { 'bindingCall': BrowserContextBindingCallEvent; + 'console': BrowserContextConsoleEvent; 'close': BrowserContextCloseEvent; + 'dialog': BrowserContextDialogEvent; 'page': BrowserContextPageEvent; 'route': BrowserContextRouteEvent; 'video': BrowserContextVideoEvent; @@ -1711,9 +1721,7 @@ 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; on(event: 'fileChooser', callback: (params: PageFileChooserEvent) => void): this; on(event: 'frameAttached', callback: (params: PageFrameAttachedEvent) => void): this; @@ -1764,13 +1772,7 @@ export type PageBindingCallEvent = { binding: BindingCallChannel, }; export type PageCloseEvent = {}; -export type PageConsoleEvent = { - message: ConsoleMessageChannel, -}; export type PageCrashEvent = {}; -export type PageDialogEvent = { - dialog: DialogChannel, -}; export type PageDownloadEvent = { url: string, suggestedFilename: string, @@ -2210,9 +2212,7 @@ export type PageUpdateSubscriptionResult = void; export interface PageEvents { 'bindingCall': PageBindingCallEvent; 'close': PageCloseEvent; - 'console': PageConsoleEvent; 'crash': PageCrashEvent; - 'dialog': PageDialogEvent; 'download': PageDownloadEvent; 'fileChooser': PageFileChooserEvent; 'frameAttached': PageFrameAttachedEvent; @@ -3702,6 +3702,7 @@ export interface WebSocketEvents { // ----------- ConsoleMessage ----------- export type ConsoleMessageInitializer = { + page: PageChannel, type: string, text: string, args: JSHandleChannel[], @@ -3754,6 +3755,7 @@ export interface BindingCallEvents { // ----------- Dialog ----------- export type DialogInitializer = { + page?: PageChannel, type: string, message: string, defaultValue: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 5947cecfbb..fbb8f73e64 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1165,8 +1165,16 @@ BrowserContext: parameters: binding: BindingCall + console: + parameters: + message: ConsoleMessage + close: + dialog: + parameters: + dialog: Dialog + page: parameters: page: Page @@ -1595,16 +1603,8 @@ Page: close: - console: - parameters: - message: ConsoleMessage - crash: - dialog: - parameters: - dialog: Dialog - download: parameters: url: string @@ -2935,6 +2935,7 @@ ConsoleMessage: type: interface initializer: + page: Page type: string text: string args: @@ -2976,6 +2977,7 @@ Dialog: type: interface initializer: + page: Page? type: string message: string defaultValue: string diff --git a/tests/library/browsercontext-events.spec.ts b/tests/library/browsercontext-events.spec.ts new file mode 100644 index 0000000000..b6f84aafc7 --- /dev/null +++ b/tests/library/browsercontext-events.spec.ts @@ -0,0 +1,162 @@ +/** + * 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); +}); + +test('dialog event should work @smoke', async ({ page }) => { + const promise = page.evaluate(() => prompt('hey?')); + const [dialog1, dialog2] = await Promise.all([ + page.context().waitForEvent('dialog'), + page.waitForEvent('dialog'), + ]); + + expect(dialog1).toBe(dialog2); + expect(dialog1.message()).toBe('hey?'); + expect(dialog1.page()).toBe(page); + await dialog1.accept('hello'); + expect(await promise).toBe('hello'); +}); + +test('dialog event should work in popup', async ({ page }) => { + const promise = page.evaluate(() => { + const win = window.open(''); + return (win as any).prompt('hey?'); + }); + + const [dialog, popup] = await Promise.all([ + page.context().waitForEvent('dialog'), + page.waitForEvent('popup'), + ]); + + expect(dialog.message()).toBe('hey?'); + expect(dialog.page()).toBe(popup); + await dialog.accept('hello'); + expect(await promise).toBe('hello'); +}); + +test('dialog event should work in popup 2', async ({ page, browserName }) => { + test.fixme(browserName === 'firefox', 'dialog from javascript: url is not reported at all'); + + const promise = page.evaluate(async () => { + window.open('javascript:prompt("hey?")'); + }); + + const dialog = await page.context().waitForEvent('dialog'); + + expect(dialog.message()).toBe('hey?'); + expect(dialog.page()).toBe(null); + await dialog.accept('hello'); + await promise; +}); + +test('dialog event should work in immdiately closed popup', async ({ page }) => { + const promise = page.evaluate(async () => { + const win = window.open(); + const result = (win as any).prompt('hey?'); + win.close(); + return result; + }); + + const [dialog, popup] = await Promise.all([ + page.context().waitForEvent('dialog'), + page.waitForEvent('popup'), + ]); + + expect(dialog.message()).toBe('hey?'); + expect(dialog.page()).toBe(popup); + await dialog.accept('hello'); + expect(await promise).toBe('hello'); +}); + +test('dialog event should work with inline script tag', async ({ page, server }) => { + server.setRoute('/popup.html', (req, res) => { + res.setHeader('content-type', 'text/html'); + res.end(``); + }); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(`Click me`); + + const promise = page.click('a'); + const [dialog, popup] = await Promise.all([ + page.context().waitForEvent('dialog'), + page.context().waitForEvent('page'), + ]); + + expect(dialog.message()).toBe('hey?'); + expect(dialog.page()).toBe(popup); + await dialog.accept('hello'); + await promise; + await expect.poll(() => popup.evaluate('window.result')).toBe('hello'); +}); diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index b89505a217..9d5580459c 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -125,7 +125,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 }) => {