feat: BrowserContext.on('dialog') (#22033)

Dialogs created early during page initialization are only reported on
the context, with `page()` being `null`.
This commit is contained in:
Dmitry Gozman 2023-03-28 13:15:55 -07:00 committed by GitHub
parent 00d98770ee
commit 3b359e27b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 342 additions and 62 deletions

View file

@ -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]>

View file

@ -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]>

View file

@ -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

View file

@ -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<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
@ -100,6 +101,19 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
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));

View file

@ -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<channels.DialogChannel> 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 {

View file

@ -37,6 +37,7 @@ export const Events = {
BrowserContext: {
Console: 'console',
Close: 'close',
Dialog: 'dialog',
Page: 'page',
BackgroundPage: 'backgroundpage',
ServiceWorker: 'serviceworker',

View file

@ -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<channels.PageChannel> 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));

View file

@ -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,

View file

@ -46,6 +46,7 @@ export abstract class BrowserContext extends SdkObject {
static Events = {
Console: 'console',
Close: 'close',
Dialog: 'dialog',
Page: 'page',
Request: 'request',
Response: 'response',

View file

@ -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,

View file

@ -41,6 +41,10 @@ export class Dialog extends SdkObject {
this._page._frameManager.dialogDidOpen(this);
}
page() {
return this._page;
}
type(): string {
return this._type;
}

View file

@ -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<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true;
@ -80,7 +81,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('close');
this._dispose();
});
this.addObjectListener(BrowserContext.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this, message) }));
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())

View file

@ -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<ConsoleMessage, channels.ConsoleMessageChannel, BrowserContextDispatcher> implements channels.ConsoleMessageChannel {
export class ConsoleMessageDispatcher extends Dispatcher<ConsoleMessage, channels.ConsoleMessageChannel, PageDispatcher> 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)),

View file

@ -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<Dialog, channels.DialogChannel, PageDispatcher> implements channels.DialogChannel {
export class DialogDispatcher extends Dispatcher<Dialog, channels.DialogChannel, BrowserContextDispatcher | PageDispatcher> 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(),

View file

@ -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<Page, channels.PageChannel, Brows
this._dispose();
});
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) });

View file

@ -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,

View file

@ -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<void>();
private _disconnected = false;
private _initialized = false;
private _consoleMessagesBeforeInitialized: ConsoleMessage[] = [];
private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = [];
readonly _disconnectedPromise = new ManualPromise<Error>();
readonly _crashedPromise = new ManualPromise<Error>();
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<network.Response | null> {

View file

@ -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);

View file

@ -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,

View file

@ -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<boolean>, timeout?: number } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
@ -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<boolean>, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise<boolean>)): Promise<ConsoleMessage>;
/**
* 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<boolean>, timeout?: number } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* 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`.
*/

View file

@ -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,

View file

@ -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

View file

@ -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(`<script>window.result = prompt('hey?')</script>`);
});
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href='popup.html' target=_blank>Click me</a>`);
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');
});