feat(dialog): add dialogclosed event (#42014)

This commit is contained in:
Dmitry Gozman
2026-07-28 16:51:57 +01:00
committed by GitHub
parent 05709a54bc
commit 974edd3feb
21 changed files with 359 additions and 6 deletions
+6
View File
@@ -198,6 +198,12 @@ Context.Dialog += async (_, dialog) =>
When no [`event: Page.dialog`] or [`event: BrowserContext.dialog`] listeners are present, all dialogs are automatically dismissed.
:::
## event: BrowserContext.dialogClosed
* since: v1.63
- argument: <[Dialog]>
Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by [`method: Dialog.accept`], by [`method: Dialog.dismiss`], or manually by the user in the headed browser.
## event: BrowserContext.download
* since: v1.60
- argument: <[Download]>
+6
View File
@@ -319,6 +319,12 @@ page.RequestFailed += (_, request) =>
When no [`event: Page.dialog`] or [`event: BrowserContext.dialog`] listeners are present, all dialogs are automatically dismissed.
:::
## event: Page.dialogClosed
* since: v1.63
- argument: <[Dialog]>
Emitted when a JavaScript dialog has been closed, either by [`method: Dialog.accept`], by [`method: Dialog.dismiss`], or manually by the user in the headed browser.
## event: Page.DOMContentLoaded
* since: v1.9
- argument: <[Page]>
+94
View File
@@ -1046,6 +1046,14 @@ export interface Page {
*/
on(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
on(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -1207,6 +1215,11 @@ export interface Page {
*/
once(event: 'dialog', listener: (dialog: Dialog) => any): 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: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
@@ -1348,6 +1361,14 @@ export interface Page {
*/
addListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
addListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -1509,6 +1530,11 @@ export interface Page {
*/
removeListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -1604,6 +1630,11 @@ export interface Page {
*/
off(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -1745,6 +1776,14 @@ export interface Page {
*/
prependListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
prependListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -5287,6 +5326,14 @@ export interface Page {
*/
waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
waitForEvent(event: 'dialogclosed', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -9222,6 +9269,14 @@ export interface BrowserContext {
*/
on(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
on(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -9367,6 +9422,11 @@ export interface BrowserContext {
*/
once(event: 'dialog', listener: (dialog: Dialog) => any): 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: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
@@ -9488,6 +9548,14 @@ export interface BrowserContext {
*/
addListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
addListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -9633,6 +9701,11 @@ export interface BrowserContext {
*/
removeListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -9718,6 +9791,11 @@ export interface BrowserContext {
*/
off(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -9839,6 +9917,14 @@ export interface BrowserContext {
*/
prependListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
prependListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -10703,6 +10789,14 @@ export interface BrowserContext {
*/
waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
waitForEvent(event: 'dialogclosed', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -156,6 +156,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
dialog.dismiss({}, kNoTimeout).catch(() => {});
}
});
this._channel.on('dialogClosed', ({ dialog }) => {
const dialogObject = Dialog.from(dialog);
this.emit(Events.BrowserContext.DialogClosed, dialogObject);
const page = dialogObject.page();
if (page)
page.emit(Events.Page.DialogClosed, dialogObject);
});
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));
@@ -171,6 +178,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this._setEventToSubscriptionMapping(new Map<string, channels.BrowserContextUpdateSubscriptionParams['event']>([
[Events.BrowserContext.Console, 'console'],
[Events.BrowserContext.Dialog, 'dialog'],
[Events.BrowserContext.DialogClosed, 'dialogClosed'],
[Events.BrowserContext.Request, 'request'],
[Events.BrowserContext.Response, 'response'],
[Events.BrowserContext.RequestFinished, 'requestFinished'],
+7 -2
View File
@@ -1210,6 +1210,7 @@ export interface BrowserContextEventTarget {
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: 'dialogClosed', callback: (params: BrowserContextDialogClosedEvent) => void): this;
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
on(event: 'pageError', callback: (params: BrowserContextPageErrorEvent) => void): this;
on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this;
@@ -1281,6 +1282,9 @@ export type BrowserContextCloseEvent = {};
export type BrowserContextDialogEvent = {
dialog: DialogChannel,
};
export type BrowserContextDialogClosedEvent = {
dialog: DialogChannel,
};
export type BrowserContextPageEvent = {
page: PageChannel,
};
@@ -1575,7 +1579,7 @@ export type BrowserContextCreateTempFilesResult = {
writableStreams: WritableStreamChannel[],
};
export type BrowserContextUpdateSubscriptionParams = {
event: 'console' | 'dialog' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
event: 'console' | 'dialog' | 'dialogClosed' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
enabled: boolean,
};
export type BrowserContextUpdateSubscriptionOptions = {
@@ -1682,6 +1686,7 @@ export interface BrowserContextEvents {
'console': BrowserContextConsoleEvent;
'close': BrowserContextCloseEvent;
'dialog': BrowserContextDialogEvent;
'dialogClosed': BrowserContextDialogClosedEvent;
'page': BrowserContextPageEvent;
'pageError': BrowserContextPageErrorEvent;
'route': BrowserContextRouteEvent;
@@ -4462,7 +4467,7 @@ export type PageScreencastStopParams = {};
export type PageScreencastStopOptions = {};
export type PageScreencastStopResult = void;
export type PageUpdateSubscriptionParams = {
event: 'console' | 'dialog' | 'fileChooser' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
event: 'console' | 'dialog' | 'dialogClosed' | 'fileChooser' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
enabled: boolean,
};
export type PageUpdateSubscriptionOptions = {
@@ -44,6 +44,7 @@ export const Events = {
Console: 'console',
Close: 'close',
Dialog: 'dialog',
DialogClosed: 'dialogclosed',
Download: 'download',
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
@@ -71,6 +72,7 @@ export const Events = {
Crash: 'crash',
Console: 'console',
Dialog: 'dialog',
DialogClosed: 'dialogclosed',
Download: 'download',
FileChooser: 'filechooser',
DOMContentLoaded: 'domcontentloaded',
@@ -178,6 +178,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._setEventToSubscriptionMapping(new Map<string, channels.PageUpdateSubscriptionParams['event']>([
[Events.Page.Console, 'console'],
[Events.Page.Dialog, 'dialog'],
[Events.Page.DialogClosed, 'dialogClosed'],
[Events.Page.Request, 'request'],
[Events.Page.Response, 'response'],
[Events.Page.RequestFinished, 'requestFinished'],
@@ -89,6 +89,7 @@ export class BidiPage implements PageDelegate {
eventsHelper.addEventListener(bidiSession, 'browsingContext.downloadWillBegin', this._onDownloadWillBegin.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.downloadEnd', this._onDownloadEnded.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.userPromptOpened', this._onUserPromptOpened.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.userPromptClosed', this._onUserPromptClosed.bind(this)),
eventsHelper.addEventListener(bidiSession, 'log.entryAdded', this._onLogEntryAdded.bind(this)),
eventsHelper.addEventListener(bidiSession, 'input.fileDialogOpened', this._onFileDialogOpened.bind(this)),
];
@@ -257,6 +258,10 @@ export class BidiPage implements PageDelegate {
event.defaultValue));
}
private _onUserPromptClosed() {
this._page.browserContext.dialogManager.dialogWasClosedInBrowser(this._page);
}
private _onDownloadWillBegin(event: bidi.BrowsingContext.DownloadWillBeginParams) {
// TODO: remove the event.navigation fallback when Chrome supports event.download
// See https://github.com/GoogleChromeLabs/chromium-bidi/issues/4155
+7 -2
View File
@@ -1211,6 +1211,7 @@ export interface BrowserContextEventTarget {
_dispatchEvent(event: 'console', params?: BrowserContextConsoleEvent): void;
_dispatchEvent(event: 'close', params?: BrowserContextCloseEvent): void;
_dispatchEvent(event: 'dialog', params?: BrowserContextDialogEvent): void;
_dispatchEvent(event: 'dialogClosed', params?: BrowserContextDialogClosedEvent): void;
_dispatchEvent(event: 'page', params?: BrowserContextPageEvent): void;
_dispatchEvent(event: 'pageError', params?: BrowserContextPageErrorEvent): void;
_dispatchEvent(event: 'route', params?: BrowserContextRouteEvent): void;
@@ -1282,6 +1283,9 @@ export type BrowserContextCloseEvent = {};
export type BrowserContextDialogEvent = {
dialog: DialogChannel,
};
export type BrowserContextDialogClosedEvent = {
dialog: DialogChannel,
};
export type BrowserContextPageEvent = {
page: PageChannel,
};
@@ -1576,7 +1580,7 @@ export type BrowserContextCreateTempFilesResult = {
writableStreams: WritableStreamChannel[],
};
export type BrowserContextUpdateSubscriptionParams = {
event: 'console' | 'dialog' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
event: 'console' | 'dialog' | 'dialogClosed' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
enabled: boolean,
};
export type BrowserContextUpdateSubscriptionOptions = {
@@ -1683,6 +1687,7 @@ export interface BrowserContextEvents {
'console': BrowserContextConsoleEvent;
'close': BrowserContextCloseEvent;
'dialog': BrowserContextDialogEvent;
'dialogClosed': BrowserContextDialogClosedEvent;
'page': BrowserContextPageEvent;
'pageError': BrowserContextPageErrorEvent;
'route': BrowserContextRouteEvent;
@@ -4463,7 +4468,7 @@ export type PageScreencastStopParams = {};
export type PageScreencastStopOptions = {};
export type PageScreencastStopResult = void;
export type PageUpdateSubscriptionParams = {
event: 'console' | 'dialog' | 'fileChooser' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
event: 'console' | 'dialog' | 'dialogClosed' | 'fileChooser' | 'request' | 'response' | 'requestFinished' | 'requestFailed',
enabled: boolean,
};
export type PageUpdateSubscriptionOptions = {
@@ -417,6 +417,7 @@ class FrameSession {
eventsHelper.addEventListener(this._client, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)),
eventsHelper.addEventListener(this._client, 'Page.frameRequestedNavigation', event => this._onFrameRequestedNavigation(event)),
eventsHelper.addEventListener(this._client, 'Page.javascriptDialogOpening', event => this._onDialog(event)),
eventsHelper.addEventListener(this._client, 'Page.javascriptDialogClosed', () => this._onDialogClosed()),
eventsHelper.addEventListener(this._client, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
eventsHelper.addEventListener(this._client, 'Runtime.bindingCalled', event => this._onBindingCalled(event)),
eventsHelper.addEventListener(this._client, 'Runtime.consoleAPICalled', event => this._onConsoleAPI(event)),
@@ -842,6 +843,10 @@ class FrameSession {
event.defaultPrompt));
}
_onDialogClosed() {
this._page.browserContext.dialogManager.dialogWasClosedInBrowser(this._page);
}
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
this._page.addPageError(exceptionToError(exceptionDetails), stackTraceToLocation(exceptionDetails.stackTrace));
}
@@ -33,6 +33,7 @@ export class Dialog extends SdkObject {
private _message: string;
private _onHandle: OnHandle;
private _handled = false;
private _closed = false;
private _defaultValue: string;
constructor(page: Page, type: DialogType, message: string, onHandle: OnHandle, defaultValue?: string) {
@@ -73,6 +74,7 @@ export class Dialog extends SdkObject {
this._handled = true;
this._page.browserContext.dialogManager._dialogWillClose(this);
await this._onHandle(true, promptText);
this._didClose();
}
async _dismiss() {
@@ -80,6 +82,14 @@ export class Dialog extends SdkObject {
this._handled = true;
this._page.browserContext.dialogManager._dialogWillClose(this);
await this._onHandle(false);
this._didClose();
}
_didClose() {
if (this._closed)
return;
this._closed = true;
this._page.browserContext.dialogManager._dialogDidClose(this);
}
async _close() {
@@ -93,6 +103,7 @@ export class Dialog extends SdkObject {
export class DialogManager {
private _instrumentation: Instrumentation;
private _dialogHandlers = new Set<(dialog: Dialog) => boolean>();
private _dialogClosedListeners = new Set<(dialog: Dialog) => void>();
private _openedDialogs = new Set<Dialog>();
constructor(instrumentation: Instrumentation) {
@@ -119,6 +130,27 @@ export class DialogManager {
this._openedDialogs.delete(dialog);
}
_dialogDidClose(dialog: Dialog) {
this._openedDialogs.delete(dialog);
for (const listener of this._dialogClosedListeners)
listener(dialog);
}
dialogWasClosedInBrowser(page: Page) {
for (const dialog of this._openedDialogs) {
if (dialog.page() === page)
dialog._didClose();
}
}
addDialogClosedListener(listener: (dialog: Dialog) => void) {
this._dialogClosedListeners.add(listener);
}
removeDialogClosedListener(listener: (dialog: Dialog) => void) {
this._dialogClosedListeners.delete(listener);
}
addDialogHandler(handler: (dialog: Dialog) => boolean) {
this._dialogHandlers.add(handler);
}
@@ -57,6 +57,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
_webSocketInterceptionPatterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
private _disposables: Disposable[] = [];
private _dialogHandler: (dialog: Dialog) => boolean;
private _dialogClosedListener: (dialog: Dialog) => void;
private _clockPaused = false;
private _requestInterceptor: RouteHandler;
private _interceptionUrlMatchers: URLMatch[] = [];
@@ -138,6 +139,13 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return true;
};
context.dialogManager.addDialogHandler(this._dialogHandler);
this._dialogClosedListener = dialog => {
if (!this._shouldDispatchEvent(dialog.page(), 'dialogClosed'))
return;
const dialogDispatcher = this.connection.existingDispatcher<DialogDispatcher>(dialog) || new DialogDispatcher(this, dialog);
this._dispatchEvent('dialogClosed', { dialog: dialogDispatcher });
};
context.dialogManager.addDialogClosedListener(this._dialogClosedListener);
if (context._browser.options.name === 'chromium' && this._object._browser instanceof CRBrowser) {
for (const serviceWorker of (context as CRBrowserContext).serviceWorkers())
@@ -444,6 +452,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
// Cleanup properly and leave the page in a good state. Other clients may still connect and use it.
this._context.dialogManager.removeDialogHandler(this._dialogHandler);
this._context.dialogManager.removeDialogClosedListener(this._dialogClosedListener);
this._interceptionUrlMatchers = [];
this._context.removeRequestInterceptor(this._requestInterceptor).catch(() => {});
disposeAll(this._disposables).catch(() => {});
@@ -86,6 +86,7 @@ export class FFPage implements PageDelegate {
eventsHelper.addEventListener(this._session, 'Page.uncaughtError', this._onUncaughtError.bind(this)),
eventsHelper.addEventListener(this._session, 'Runtime.console', this._onConsole.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.dialogClosed', this._onDialogClosed.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.workerCreated', this._onWorkerCreated.bind(this)),
@@ -301,6 +302,10 @@ export class FFPage implements PageDelegate {
params.defaultValue));
}
_onDialogClosed() {
this._page.browserContext.dialogManager.dialogWasClosedInBrowser(this._page);
}
async _onBindingCalled(event: Protocol.Page.bindingCalledPayload) {
const pageOrError = await this._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
@@ -390,6 +390,7 @@ export class WKPage implements PageDelegate {
eventsHelper.addEventListener(this._session, 'Console.messageAdded', event => this._onConsoleMessage(event)),
eventsHelper.addEventListener(this._session, 'Console.messageRepeatCountUpdated', event => this._onConsoleRepeatCountUpdated(event)),
eventsHelper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogOpening', event => this._onDialog(event)),
eventsHelper.addEventListener(this._pageProxySession, 'Dialog.javascriptDialogClosed', () => this._onDialogClosed()),
eventsHelper.addEventListener(this._session, 'Page.fileChooserOpened', event => this._onFileChooserOpened(event)),
eventsHelper.addEventListener(this._session, 'Network.requestWillBeSent', e => this._onRequestWillBeSent(this._session, e)),
eventsHelper.addEventListener(this._session, 'Network.requestIntercepted', e => this._onRequestIntercepted(this._session, e)),
@@ -625,6 +626,10 @@ export class WKPage implements PageDelegate {
event.defaultPrompt));
}
_onDialogClosed() {
this._page.browserContext.dialogManager.dialogWasClosedInBrowser(this._page);
}
private async _onFileChooserOpened(event: {frameId: Protocol.Network.FrameId, element: Protocol.Runtime.RemoteObject}) {
let handle;
try {
+94
View File
@@ -1046,6 +1046,14 @@ export interface Page {
*/
on(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
on(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -1207,6 +1215,11 @@ export interface Page {
*/
once(event: 'dialog', listener: (dialog: Dialog) => any): 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: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
@@ -1348,6 +1361,14 @@ export interface Page {
*/
addListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
addListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -1509,6 +1530,11 @@ export interface Page {
*/
removeListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -1604,6 +1630,11 @@ export interface Page {
*/
off(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -1745,6 +1776,14 @@ export interface Page {
*/
prependListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
prependListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -5287,6 +5326,14 @@ export interface Page {
*/
waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when a JavaScript dialog has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
waitForEvent(event: 'dialogclosed', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when the JavaScript
* [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) event is dispatched.
@@ -9222,6 +9269,14 @@ export interface BrowserContext {
*/
on(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
on(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -9367,6 +9422,11 @@ export interface BrowserContext {
*/
once(event: 'dialog', listener: (dialog: Dialog) => any): 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: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
@@ -9488,6 +9548,14 @@ export interface BrowserContext {
*/
addListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
addListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -9633,6 +9701,11 @@ export interface BrowserContext {
*/
removeListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -9718,6 +9791,11 @@ export interface BrowserContext {
*/
off(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Removes an event listener added by `on` or `addListener`.
*/
@@ -9839,6 +9917,14 @@ export interface BrowserContext {
*/
prependListener(event: 'dialog', listener: (dialog: Dialog) => any): this;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
prependListener(event: 'dialogclosed', listener: (dialog: Dialog) => any): this;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -10703,6 +10789,14 @@ export interface BrowserContext {
*/
waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when a JavaScript dialog in any page belonging to this context has been closed, either by
* [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialog-accept), by
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialog-dismiss), or manually by the user in the
* headed browser.
*/
waitForEvent(event: 'dialogclosed', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise<boolean>, timeout?: number, signal?: AbortSignal } | ((dialog: Dialog) => boolean | Promise<boolean>)): Promise<Dialog>;
/**
* Emitted when attachment download started in any page belonging to this context. User can access basic file
* operations on downloaded content via the passed [Download](https://playwright.dev/docs/api/class-download)
@@ -273,6 +273,7 @@ BrowserContext:
literals:
- console
- dialog
- dialogClosed
- request
- response
- requestFinished
@@ -369,6 +370,10 @@ BrowserContext:
parameters:
dialog: Dialog
dialogClosed:
parameters:
dialog: Dialog
page:
parameters:
page: Page
+1
View File
@@ -603,6 +603,7 @@ Page:
literals:
- console
- dialog
- dialogClosed
- fileChooser
- request
- response
+5 -2
View File
@@ -670,6 +670,9 @@ scheme.BrowserContextCloseEvent = tOptional(tObject({}));
scheme.BrowserContextDialogEvent = tObject({
dialog: tChannel(['Dialog']),
});
scheme.BrowserContextDialogClosedEvent = tObject({
dialog: tChannel(['Dialog']),
});
scheme.BrowserContextPageEvent = tObject({
page: tChannel(['Page']),
});
@@ -870,7 +873,7 @@ scheme.BrowserContextCreateTempFilesResult = tObject({
writableStreams: tArray(tChannel(['WritableStream'])),
});
scheme.BrowserContextUpdateSubscriptionParams = tObject({
event: tEnum(['console', 'dialog', 'request', 'response', 'requestFinished', 'requestFailed']),
event: tEnum(['console', 'dialog', 'dialogClosed', 'request', 'response', 'requestFinished', 'requestFailed']),
enabled: tBoolean,
});
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
@@ -2592,7 +2595,7 @@ scheme.PageScreencastFrameAckResult = tOptional(tObject({}));
scheme.PageScreencastStopParams = tOptional(tObject({}));
scheme.PageScreencastStopResult = tOptional(tObject({}));
scheme.PageUpdateSubscriptionParams = tObject({
event: tEnum(['console', 'dialog', 'fileChooser', 'request', 'response', 'requestFinished', 'requestFailed']),
event: tEnum(['console', 'dialog', 'dialogClosed', 'fileChooser', 'request', 'response', 'requestFinished', 'requestFailed']),
enabled: tBoolean,
});
scheme.PageUpdateSubscriptionResult = tOptional(tObject({}));
@@ -100,6 +100,19 @@ test('dialog event should work @smoke', async ({ page }) => {
expect(await promise).toBe('hello');
});
test('dialogclosed event should work', async ({ page }) => {
const promise = page.evaluate(() => prompt('hey?'));
const dialog = await page.context().waitForEvent('dialog');
const [closed1, closed2] = await Promise.all([
page.context().waitForEvent('dialogclosed'),
page.waitForEvent('dialogclosed'),
dialog.accept('hello'),
]);
expect(closed1).toBe(dialog);
expect(closed2).toBe(dialog);
expect(await promise).toBe('hello');
});
test('dialog event should work in popup', async ({ page }) => {
const promise = page.evaluate(() => {
const win = window.open('');
+14
View File
@@ -746,3 +746,17 @@ test('should capture console.log from ServiceWorker start', async ({ context, pa
expect(consoleMessage.text()).toBe('Hello from the first line of sw.js');
expect(consoleMessage.type()).toBe('log');
});
test('should fire dialogclosed event when dialog is closed out of band', async ({ page }) => {
// Establish the CDP session up front: creating one while a dialog is blocking the page hangs.
const client = await page.context().newCDPSession(page);
await client.send('Page.enable');
const dialogPromise = page.waitForEvent('dialog');
const closedPromise = page.waitForEvent('dialogclosed');
const evaluatePromise = page.evaluate(() => alert('yo'));
const dialog = await dialogPromise;
// Handle the dialog out of band, similar to the user closing it in the headed browser.
await client.send('Page.handleJavaScriptDialog', { accept: true });
expect(await closedPromise).toBe(dialog);
await evaluatePromise;
});
+35
View File
@@ -17,6 +17,8 @@
import { test as it, expect } from './pageTest';
import type { Dialog } from 'playwright-core';
it('should fire', async ({ page, server }) => {
page.on('dialog', dialog => {
expect(dialog.type()).toBe('alert');
@@ -27,6 +29,39 @@ it('should fire', async ({ page, server }) => {
await page.evaluate(() => alert('yo'));
});
it('should fire dialogclosed when dialog is accepted', async ({ page }) => {
const closed: Dialog[] = [];
page.on('dialogclosed', dialog => closed.push(dialog));
let opened: Dialog | undefined;
page.on('dialog', dialog => {
opened = dialog;
void dialog.accept();
});
await page.evaluate(() => alert('yo'));
await expect.poll(() => closed.length).toBe(1);
expect(closed[0]).toBe(opened);
// Perform some roundtrips to ensure the event does not fire twice.
await page.evaluate(() => 1);
await page.evaluate(() => 1);
expect(closed.length).toBe(1);
});
it('should fire dialogclosed when dialog is dismissed', async ({ page }) => {
const closedPromise = page.waitForEvent('dialogclosed');
page.on('dialog', dialog => void dialog.dismiss());
await page.evaluate(() => confirm('boolean?'));
const dialog = await closedPromise;
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toBe('boolean?');
});
it('should fire dialogclosed for auto-dismissed dialogs', async ({ page }) => {
const closedPromise = page.waitForEvent('dialogclosed');
await page.evaluate(() => alert('yo'));
const dialog = await closedPromise;
expect(dialog.message()).toBe('yo');
});
it('should allow accepting prompts @smoke', async ({ page, isElectron }) => {
it.skip(isElectron, 'prompt() is not a thing in electron');