diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index fc798f1224..3ae900ff37 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -154,6 +154,42 @@ context.Console += async (_, msg) => 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-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 0b5dac85e2..4892f530cf 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -285,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(); @@ -309,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 0b94b9339a..3bb1d6bdc2 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -41,6 +41,7 @@ import { createInstrumentation } from './clientInstrumentation'; 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(); @@ -100,6 +101,19 @@ export class BrowserContext extends ChannelOwner 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/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 55d8848c28..ceca0829d6 100644 --- a/packages/playwright-core/src/client/events.ts +++ b/packages/playwright-core/src/client/events.ts @@ -37,6 +37,7 @@ 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 db24693072..ff6d15f6cd 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -32,7 +32,6 @@ import type { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { evaluationScript } from './clientHelper'; import { Coverage } from './coverage'; -import { Dialog } from './dialog'; import { Download } from './download'; import { determineScreenshotType, ElementHandle } from './elementHandle'; import { Events } from './events'; @@ -124,15 +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('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 afc37f4d26..101969bb64 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -759,6 +759,9 @@ scheme.BrowserContextConsoleEvent = tObject({ message: tChannel(['ConsoleMessage']), }); scheme.BrowserContextCloseEvent = tOptional(tObject({})); +scheme.BrowserContextDialogEvent = tObject({ + dialog: tChannel(['Dialog']), +}); scheme.BrowserContextPageEvent = tObject({ page: tChannel(['Page']), }); @@ -933,9 +936,6 @@ scheme.PageBindingCallEvent = tObject({ }); scheme.PageCloseEvent = tOptional(tObject({})); scheme.PageCrashEvent = tOptional(tObject({})); -scheme.PageDialogEvent = tObject({ - dialog: tChannel(['Dialog']), -}); scheme.PageDownloadEvent = tObject({ url: tString, suggestedFilename: tString, @@ -2097,6 +2097,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 910d729811..c92b671304 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -46,6 +46,7 @@ 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/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 4f815c560b..5ac9649588 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -34,6 +34,7 @@ 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; @@ -80,7 +81,8 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this, message) })); + this.addObjectListener(BrowserContext.Events.Console, message => 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 c8228811da..35354a5ed5 100644 --- a/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/consoleMessageDispatcher.ts @@ -16,17 +16,15 @@ import type { ConsoleMessage } from '../console'; import type * as channels from '@protocol/channels'; -import { PageDispatcher } from './pageDispatcher'; -import type { BrowserContextDispatcher } from './browserContextDispatcher'; +import type { PageDispatcher } from './pageDispatcher'; 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: BrowserContextDispatcher, message: ConsoleMessage) { - const page = PageDispatcher.from(scope, message.page()); - 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(page, a)), 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 290a176a4f..3e249692e2 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 { DialogDispatcher } from './dialogDispatcher'; import { FrameDispatcher } from './frameDispatcher'; import { RequestDispatcher } from './networkDispatchers'; import { ResponseDispatcher } from './networkDispatchers'; @@ -76,7 +75,6 @@ export class PageDispatcher extends Dispatcher 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/page.ts b/packages/playwright-core/src/server/page.ts index 59269f17de..0344d73962 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', - Dialog: 'dialog', Download: 'download', FileChooser: 'filechooser', // Can't use just 'error' due to node.js special treatment of error events. @@ -140,7 +139,7 @@ export class Page extends SdkObject { private _closedPromise = new ManualPromise(); private _disconnected = false; private _initialized = false; - private _consoleMessagesBeforeInitialized: ConsoleMessage[] = []; + private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = []; readonly _disconnectedPromise = new ManualPromise(); readonly _crashedPromise = new ManualPromise(); readonly _browserContext: BrowserContext; @@ -209,9 +208,9 @@ export class Page extends SdkObject { this._initialized = true; this.emitOnContext(contextEvent, this); - for (const message of this._consoleMessagesBeforeInitialized) - this.emitOnContext(BrowserContext.Events.Console, message); - this._consoleMessagesBeforeInitialized = []; + 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 @@ -232,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); @@ -361,14 +373,7 @@ export class Page extends SdkObject { 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._consoleMessagesBeforeInitialized.push(message); + 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 0dfbfab0b0..ddf5d44ec0 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 }; @@ -425,9 +426,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. @@ -471,7 +473,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 efdf3c5ab2..8c838a0c2f 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -922,14 +922,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; @@ -1215,14 +1218,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; @@ -1603,14 +1609,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; @@ -4241,14 +4250,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; @@ -7470,6 +7482,27 @@ export interface BrowserContext { */ 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 @@ -7553,6 +7586,11 @@ export interface BrowserContext { */ 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. */ @@ -7624,6 +7662,27 @@ export interface BrowserContext { */ 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 @@ -7707,6 +7766,11 @@ export interface BrowserContext { */ 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`. */ @@ -7752,6 +7816,11 @@ export interface BrowserContext { */ 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`. */ @@ -7823,6 +7892,27 @@ export interface BrowserContext { */ 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 @@ -8383,6 +8473,27 @@ export interface BrowserContext { */ 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 @@ -16386,6 +16497,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 bcc99f0a35..e7ed6eb8c9 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1396,6 +1396,7 @@ 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; @@ -1440,6 +1441,9 @@ export type BrowserContextConsoleEvent = { message: ConsoleMessageChannel, }; export type BrowserContextCloseEvent = {}; +export type BrowserContextDialogEvent = { + dialog: DialogChannel, +}; export type BrowserContextPageEvent = { page: PageChannel, }; @@ -1683,6 +1687,7 @@ export interface BrowserContextEvents { 'bindingCall': BrowserContextBindingCallEvent; 'console': BrowserContextConsoleEvent; 'close': BrowserContextCloseEvent; + 'dialog': BrowserContextDialogEvent; 'page': BrowserContextPageEvent; 'route': BrowserContextRouteEvent; 'video': BrowserContextVideoEvent; @@ -1708,7 +1713,6 @@ export interface PageEventTarget { on(event: 'bindingCall', callback: (params: PageBindingCallEvent) => void): this; on(event: 'close', callback: (params: PageCloseEvent) => 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; @@ -1760,9 +1764,6 @@ export type PageBindingCallEvent = { }; export type PageCloseEvent = {}; export type PageCrashEvent = {}; -export type PageDialogEvent = { - dialog: DialogChannel, -}; export type PageDownloadEvent = { url: string, suggestedFilename: string, @@ -2203,7 +2204,6 @@ export interface PageEvents { 'bindingCall': PageBindingCallEvent; 'close': PageCloseEvent; 'crash': PageCrashEvent; - 'dialog': PageDialogEvent; 'download': PageDownloadEvent; 'fileChooser': PageFileChooserEvent; 'frameAttached': PageFrameAttachedEvent; @@ -3740,6 +3740,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 bbefbbded6..6741fce732 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1163,6 +1163,10 @@ BrowserContext: close: + dialog: + parameters: + dialog: Dialog + page: parameters: page: Page @@ -1579,10 +1583,6 @@ Page: crash: - dialog: - parameters: - dialog: Dialog - download: parameters: url: string @@ -2918,6 +2918,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 index 51b30688cb..b6f84aafc7 100644 --- a/tests/library/browsercontext-events.spec.ts +++ b/tests/library/browsercontext-events.spec.ts @@ -73,3 +73,90 @@ test('console event should work in immediately closed popup', async ({ page, bro 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'); +});