Skip to content

Commit

Permalink
feat(dialogs): auto-dismiss dialogs when there are no listeners (#5269)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dgozman committed Feb 3, 2021
1 parent bbfbb1b commit 53ed35e
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 34 deletions.
5 changes: 5 additions & 0 deletions docs/src/api/class-dialog.md
Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions docs/src/api/class-page.md
Expand Up @@ -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]>
Expand Down Expand Up @@ -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`]:
Expand Down
18 changes: 9 additions & 9 deletions docs/src/dialogs.md
Expand Up @@ -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());
Expand All @@ -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:
Expand All @@ -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`]
Expand Down
1 change: 1 addition & 0 deletions src/cli/cli.ts
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/client/page.ts
Expand Up @@ -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)));
Expand Down
15 changes: 12 additions & 3 deletions test/page-dialog.spec.ts
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 changes: 54 additions & 18 deletions types/types.d.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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>;

Expand Down Expand Up @@ -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 {
/**
Expand Down

0 comments on commit 53ed35e

Please sign in to comment.