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 {
   /**