From 53ed35ef96578379ba86e95e58631571a0b59f8d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman <dgozman@gmail.com> Date: Wed, 3 Feb 2021 10:34:45 -0800 Subject: [PATCH] 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. --- docs/src/api/class-dialog.md | 5 +++ docs/src/api/class-page.md | 9 +++-- docs/src/dialogs.md | 18 ++++----- src/cli/cli.ts | 1 + src/client/page.ts | 5 ++- test/page-dialog.spec.ts | 15 ++++++-- types/types.d.ts | 72 +++++++++++++++++++++++++++--------- 7 files changed, 91 insertions(+), 34 deletions(-) diff --git a/docs/src/api/class-dialog.md b/docs/src/api/class-dialog.md index e093705b264d7..b2951d6748e73 100644 --- a/docs/src/api/class-dialog.md +++ b/docs/src/api/class-dialog.md @@ -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. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index bc8da6fafb077..bfcff235fe493 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -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`]: diff --git a/docs/src/dialogs.md b/docs/src/dialogs.md index 0ec1ca835b6ed..f85924aecdaf4 100644 --- a/docs/src/dialogs.md +++ b/docs/src/dialogs.md @@ -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`] diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 705cee68c1604..569c2c2b19a12 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -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) diff --git a/src/client/page.ts b/src/client/page.ts index 1629e417de64e..2db7550c3e004 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -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))); diff --git a/test/page-dialog.spec.ts b/test/page-dialog.spec.ts index 666386621be6c..cf6ca3017c67f 100644 --- a/test/page-dialog.spec.ts +++ b/test/page-dialog.spec.ts @@ -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); +}); diff --git a/types/types.d.ts b/types/types.d.ts index b8f143da4ee0e..d4b526e1b6faf 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -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 { /**