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:
Dmitry Gozman 2021-02-03 10:34:45 -08:00 committed by GitHub
parent bbfbb1b2f7
commit 53ed35ef96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 91 additions and 34 deletions

View File

@ -60,6 +60,11 @@ with sync_playwright() as 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
Returns when the dialog has been accepted.

View File

@ -171,8 +171,11 @@ except Error as e:
## event: Page.dialog
- type: <[Dialog]>
Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
to the dialog via [`method: Dialog.accept`] or [`method: Dialog.dismiss`] methods.
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.
:::note
When no [`event: Page.dialog`] listeners are present, all dialogs are automatically dismissed.
:::
## event: Page.domcontentloaded
- 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.
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`.
Passing argument to [`param: expression`]:

View File

@ -9,7 +9,7 @@ Playwright can interact with the web page dialogs such as [`alert`](https://deve
## 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
page.on('dialog', dialog => dialog.accept());
@ -27,7 +27,7 @@ page.click("button")
```
:::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:
@ -37,24 +37,24 @@ WRONG!
:::
```js
page.on('dialog', dialog => console.log(dialog.message()));
await page.click('button'); // Will hang here
page.on('dialog', dialog => dialog.accept())
```
:::warn
WRONG!
:::
```python async
page.on("dialog", lambda dialog: print(dialog.message))
await page.click("button") # Will hang here
page.on("dialog", lambda dialog: dialog.accept())
```
```python sync
page.on("dialog", lambda dialog: print(dialog.message))
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
- [`Dialog`]

View File

@ -284,6 +284,7 @@ async function launchContext(options: Options, headless: boolean): Promise<{ bro
}
context.on('page', page => {
page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed.
page.on('close', () => {
const hasPage = browser.contexts().some(context => context.pages().length > 0);
if (hasPage)

View File

@ -116,7 +116,10 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('close', () => this._onClose());
this._channel.on('console', ({ message }) => this.emit(Events.Page.Console, ConsoleMessage.from(message)));
this._channel.on('crash', () => this._onCrash());
this._channel.on('dialog', ({ dialog }) => 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('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)));

View File

@ -62,9 +62,7 @@ it('should dismiss the confirm prompt', async ({page}) => {
expect(result).toBe(false);
});
it('should be able to close context with open alert', (test, { browserName, platform }) => {
test.fixme(browserName === 'webkit' && platform === 'darwin');
}, async ({context}) => {
it('should be able to close context with open alert', async ({context}) => {
const page = await context.newPage();
const alertPromise = page.waitForEvent('dialog');
await page.evaluate(() => {
@ -102,3 +100,14 @@ it('should handle multiple confirms', async ({page}) => {
`);
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
View File

@ -411,9 +411,14 @@ export interface Page {
on(event: 'crash', listener: (page: Page) => void): this;
/**
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
* 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#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.
*
* > 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;
@ -580,9 +585,14 @@ export interface Page {
once(event: 'crash', listener: (page: Page) => void): this;
/**
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
* 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#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.
*
* > 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;
@ -749,9 +759,14 @@ export interface Page {
addListener(event: 'crash', listener: (page: Page) => void): this;
/**
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
* 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#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.
*
* > 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;
@ -918,9 +933,14 @@ export interface Page {
removeListener(event: 'crash', listener: (page: Page) => void): this;
/**
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
* 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#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.
*
* > 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;
@ -1087,9 +1107,14 @@ export interface Page {
off(event: 'crash', listener: (page: Page) => void): this;
/**
* Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
* 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#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.
*
* > 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;
@ -2856,9 +2881,14 @@ export interface 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
* to the dialog via [dialog.accept([promptText])](https://playwright.dev/docs/api/class-dialog#dialogacceptprompttext) or
* [dialog.dismiss()](https://playwright.dev/docs/api/class-dialog#dialogdismiss) methods.
* 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#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.
*
* > 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>;
@ -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 {
/**