feat(dialogs): auto-dismiss dialogs when there are no listeners (#5269)
This makes dialogs disappear and prevents stalling. Pros: - No need to worry about dialogs for most users. - Those that wait for a specific dialog still get to control it. Cons: - Those who use Playwright to show interactive browser will have to add an empty 'dialog' handler to prevent auto-dismiss. We do this in cli.
This commit is contained in:
parent
bbfbb1b2f7
commit
53ed35ef96
|
|
@ -60,6 +60,11 @@ with sync_playwright() as playwright:
|
||||||
run(playwright)
|
run(playwright)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Dialogs are dismissed automatically, unless there is a [`event: Page.dialog`] listener.
|
||||||
|
When listener is present, it **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.
|
||||||
|
:::
|
||||||
|
|
||||||
## async method: Dialog.accept
|
## async method: Dialog.accept
|
||||||
|
|
||||||
Returns when the dialog has been accepted.
|
Returns when the dialog has been accepted.
|
||||||
|
|
|
||||||
|
|
@ -171,8 +171,11 @@ except Error as e:
|
||||||
## event: Page.dialog
|
## event: Page.dialog
|
||||||
- type: <[Dialog]>
|
- type: <[Dialog]>
|
||||||
|
|
||||||
Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
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.
|
||||||
to the dialog via [`method: Dialog.accept`] or [`method: Dialog.dismiss`] methods.
|
|
||||||
|
:::note
|
||||||
|
When no [`event: Page.dialog`] listeners are present, all dialogs are automatically dismissed.
|
||||||
|
:::
|
||||||
|
|
||||||
## event: Page.domcontentloaded
|
## event: Page.domcontentloaded
|
||||||
- type: <[Page]>
|
- type: <[Page]>
|
||||||
|
|
@ -808,7 +811,7 @@ If the function passed to the [`method: Page.evaluate`] returns a [Promise], the
|
||||||
for the promise to resolve and return its value.
|
for the promise to resolve and return its value.
|
||||||
|
|
||||||
If the function passed to the [`method: Page.evaluate`] returns a non-[Serializable] value, then
|
If the function passed to the [`method: Page.evaluate`] returns a non-[Serializable] value, then
|
||||||
[`method: Page.evaluate`] resolves to `undefined`. Playwright also supports transferring some
|
[`method: Page.evaluate`] resolves to `undefined`. Playwright also supports transferring some
|
||||||
additional values that are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`.
|
additional values that are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`.
|
||||||
|
|
||||||
Passing argument to [`param: expression`]:
|
Passing argument to [`param: expression`]:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ Playwright can interact with the web page dialogs such as [`alert`](https://deve
|
||||||
|
|
||||||
## alert(), confirm(), prompt() dialogs
|
## alert(), confirm(), prompt() dialogs
|
||||||
|
|
||||||
You can register a dialog handler before the action that triggers the dialog to accept or decline it.
|
By default, dialogs are auto-dismissed by Playwright, so you don't have to handle them. However, you can register a dialog handler before the action that triggers the dialog to accept or decline it.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
page.on('dialog', dialog => dialog.accept());
|
page.on('dialog', dialog => dialog.accept());
|
||||||
|
|
@ -27,7 +27,7 @@ page.click("button")
|
||||||
```
|
```
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
If your action, be it [`method: Page.click`], [`method: Page.evaluate`] or any other, results in a dialog, the action will stall until the dialog is handled. That's because dialogs in Web are modal and block further page execution until they are handled.
|
[`event: Page.dialog`] listener **must handle** the dialog. Otherwise your action will stall, be it [`method: Page.click`], [`method: Page.evaluate`] or any other. That's because dialogs in Web are modal and block further page execution until they are handled.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
As a result, following snippet will never resolve:
|
As a result, following snippet will never resolve:
|
||||||
|
|
@ -37,24 +37,24 @@ WRONG!
|
||||||
:::
|
:::
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
page.on('dialog', dialog => console.log(dialog.message()));
|
||||||
await page.click('button'); // Will hang here
|
await page.click('button'); // Will hang here
|
||||||
page.on('dialog', dialog => dialog.accept())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warn
|
|
||||||
WRONG!
|
|
||||||
:::
|
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
|
page.on("dialog", lambda dialog: print(dialog.message))
|
||||||
await page.click("button") # Will hang here
|
await page.click("button") # Will hang here
|
||||||
page.on("dialog", lambda dialog: dialog.accept())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
|
page.on("dialog", lambda dialog: print(dialog.message))
|
||||||
page.click("button") # Will hang here
|
page.click("button") # Will hang here
|
||||||
page.on("dialog", lambda dialog: dialog.accept())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
If there is no listener for [`event: Page.dialog`], all dialogs are automatically dismissed.
|
||||||
|
:::
|
||||||
|
|
||||||
### API reference
|
### API reference
|
||||||
|
|
||||||
- [`Dialog`]
|
- [`Dialog`]
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@ async function launchContext(options: Options, headless: boolean): Promise<{ bro
|
||||||
}
|
}
|
||||||
|
|
||||||
context.on('page', page => {
|
context.on('page', page => {
|
||||||
|
page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed.
|
||||||
page.on('close', () => {
|
page.on('close', () => {
|
||||||
const hasPage = browser.contexts().some(context => context.pages().length > 0);
|
const hasPage = browser.contexts().some(context => context.pages().length > 0);
|
||||||
if (hasPage)
|
if (hasPage)
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
this._channel.on('close', () => this._onClose());
|
this._channel.on('close', () => this._onClose());
|
||||||
this._channel.on('console', ({ message }) => this.emit(Events.Page.Console, ConsoleMessage.from(message)));
|
this._channel.on('console', ({ message }) => this.emit(Events.Page.Console, ConsoleMessage.from(message)));
|
||||||
this._channel.on('crash', () => this._onCrash());
|
this._channel.on('crash', () => this._onCrash());
|
||||||
this._channel.on('dialog', ({ dialog }) => this.emit(Events.Page.Dialog, Dialog.from(dialog)));
|
this._channel.on('dialog', ({ dialog }) => {
|
||||||
|
if (!this.emit(Events.Page.Dialog, Dialog.from(dialog)))
|
||||||
|
dialog.dismiss().catch(() => {});
|
||||||
|
});
|
||||||
this._channel.on('domcontentloaded', () => this.emit(Events.Page.DOMContentLoaded, this));
|
this._channel.on('domcontentloaded', () => this.emit(Events.Page.DOMContentLoaded, this));
|
||||||
this._channel.on('download', ({ download }) => this.emit(Events.Page.Download, Download.from(download)));
|
this._channel.on('download', ({ download }) => this.emit(Events.Page.Download, Download.from(download)));
|
||||||
this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple)));
|
this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple)));
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,7 @@ it('should dismiss the confirm prompt', async ({page}) => {
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to close context with open alert', (test, { browserName, platform }) => {
|
it('should be able to close context with open alert', async ({context}) => {
|
||||||
test.fixme(browserName === 'webkit' && platform === 'darwin');
|
|
||||||
}, async ({context}) => {
|
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const alertPromise = page.waitForEvent('dialog');
|
const alertPromise = page.waitForEvent('dialog');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
|
@ -102,3 +100,14 @@ it('should handle multiple confirms', async ({page}) => {
|
||||||
`);
|
`);
|
||||||
expect(await page.textContent('p')).toBe('Hello World');
|
expect(await page.textContent('p')).toBe('Hello World');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should auto-dismiss the prompt without listeners', async ({page}) => {
|
||||||
|
const result = await page.evaluate(() => prompt('question?'));
|
||||||
|
expect(result).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should auto-dismiss the alert without listeners', async ({page}) => {
|
||||||
|
await page.setContent(`<div onclick="window.alert(123); window._clicked=true">Click me</div>`);
|
||||||
|
await page.click('div');
|
||||||
|
expect(await page.evaluate('window._clicked')).toBe(true);
|
||||||
|
});
|
||||||
|
|
|
||||||
72
types/types.d.ts
vendored
72
types/types.d.ts
vendored
|
|
@ -411,9 +411,14 @@ export interface Page {
|
||||||
on(event: 'crash', listener: (page: Page) => void): this;
|
on(event: 'crash', listener: (page: Page) => void): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must**
|
||||||
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
* either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
|
*
|
||||||
|
* > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all
|
||||||
|
* dialogs are automatically dismissed.
|
||||||
*/
|
*/
|
||||||
on(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
on(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
||||||
|
|
||||||
|
|
@ -580,9 +585,14 @@ export interface Page {
|
||||||
once(event: 'crash', listener: (page: Page) => void): this;
|
once(event: 'crash', listener: (page: Page) => void): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must**
|
||||||
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
* either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
|
*
|
||||||
|
* > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all
|
||||||
|
* dialogs are automatically dismissed.
|
||||||
*/
|
*/
|
||||||
once(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
once(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
||||||
|
|
||||||
|
|
@ -749,9 +759,14 @@ export interface Page {
|
||||||
addListener(event: 'crash', listener: (page: Page) => void): this;
|
addListener(event: 'crash', listener: (page: Page) => void): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must**
|
||||||
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
* either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
|
*
|
||||||
|
* > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all
|
||||||
|
* dialogs are automatically dismissed.
|
||||||
*/
|
*/
|
||||||
addListener(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
addListener(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
||||||
|
|
||||||
|
|
@ -918,9 +933,14 @@ export interface Page {
|
||||||
removeListener(event: 'crash', listener: (page: Page) => void): this;
|
removeListener(event: 'crash', listener: (page: Page) => void): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must**
|
||||||
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
* either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
|
*
|
||||||
|
* > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all
|
||||||
|
* dialogs are automatically dismissed.
|
||||||
*/
|
*/
|
||||||
removeListener(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
removeListener(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
||||||
|
|
||||||
|
|
@ -1087,9 +1107,14 @@ export interface Page {
|
||||||
off(event: 'crash', listener: (page: Page) => void): this;
|
off(event: 'crash', listener: (page: Page) => void): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must**
|
||||||
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
* either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
|
*
|
||||||
|
* > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all
|
||||||
|
* dialogs are automatically dismissed.
|
||||||
*/
|
*/
|
||||||
off(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
off(event: 'dialog', listener: (dialog: Dialog) => void): this;
|
||||||
|
|
||||||
|
|
@ -2856,9 +2881,14 @@ export interface Page {
|
||||||
waitForEvent(event: 'crash', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise<Page>;
|
waitForEvent(event: 'crash', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise<Page>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
|
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must**
|
||||||
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
* either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
|
*
|
||||||
|
* > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all
|
||||||
|
* dialogs are automatically dismissed.
|
||||||
*/
|
*/
|
||||||
waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean, timeout?: number } | ((dialog: Dialog) => boolean)): Promise<Dialog>;
|
waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean, timeout?: number } | ((dialog: Dialog) => boolean)): Promise<Dialog>;
|
||||||
|
|
||||||
|
|
@ -8074,6 +8104,12 @@ export interface ConsoleMessage {
|
||||||
* })();
|
* })();
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
|
* > NOTE: Dialogs are dismissed automatically, unless there is a
|
||||||
|
* [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listener. When listener is present, it
|
||||||
|
* **must** either [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
|
||||||
|
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) 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.
|
||||||
*/
|
*/
|
||||||
export interface Dialog {
|
export interface Dialog {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue