skip to content

In Playwright, what happens to a window.confirm() dialog when no dialog handler is registered?

level: middleimportance: must knowfreq 70%

answer

  1. No listener means it is answered for you
  2. Dismissed, so confirm returns false
  3. A handler must answer or the page hangs
  4. Register before the click, not after
  5. accept takes optional prompt text

basics

~10 s

Playwright dismisses it automatically, so confirm() returns false and the click that opened it completes. Registering a dialog listener turns that off: your handler must then accept or dismiss, or the page freezes.

solid answer

~40 s

With no `page.on('dialog')` listener, Playwright auto-dismisses every dialog the page opens -- `alert()` returns, `confirm()` evaluates to `false`, `prompt()` returns `null` -- and the action that triggered it finishes normally. That default is what stops a stray dialog hanging the run. Registering a listener flips the contract: Playwright hands you the `Dialog` and waits, and if your callback never calls `dialog.accept()` or `dialog.dismiss()` the page stays blocked until the pending action times out. The handler must be attached **before** the action that opens the dialog, because the event fires during the click. `dialog.message()`, `dialog.type()` (`alert`, `confirm`, `prompt` or `beforeunload`) and `dialog.defaultValue()` let you assert on what the page asked before you answer it; `dialog.accept('some text')` supplies a prompt's value.

code

typescript · 16 lines
typescript
import { test, expect } from '@playwright/test';

test('deleting an issue asks for confirmation', async ({ page }) => {
  await page.goto('/issues/ISS-482');

  let asked = '';
  page.once('dialog', async dialog => {
    asked = dialog.message();
    await dialog.accept();
  });

  await page.getByRole('button', { name: 'Delete issue' }).click();

  expect(asked).toContain('Delete ISS-482');
  await expect(page.getByText('Issue deleted')).toBeVisible();
});

go deeper

for a junior

Remember the default: with no listener the dialog is dismissed, so a confirm-guarded delete quietly does not happen. Register a handler when your test needs the action to go through.

for a middle

Explain both halves of the contract -- auto-dismiss with no listener, and a listener that must accept or dismiss or the page blocks -- plus why the handler has to exist before the click.

for a senior

Diagnose from the symptom. A click timing out with a registered handler, or a delete that silently no-ops, should both point you at dialog handling rather than at locators or actionability.

for a principal

Decide the suite-wide policy: which dialogs are asserted on, which are absorbed, and how a dialog nobody expected becomes a visible signal instead of a silent pass.

## The default nobody notices A JavaScript dialog -- `alert()`, `confirm()`, `prompt()` -- blocks the page until someone answers it. That is fatal for automation: one stray `alert()` on the issue detail page would freeze every action after it. Playwright removes the hazard by default. When **no** `dialog` listener is attached to the page, every dialog the page opens is dismissed automatically and immediately, and the action that triggered it completes as normal. Concretely: - `alert()` returns and execution continues. - `confirm()` evaluates to `false`, so the cancel branch runs. - `prompt()` returns `null`. - A `beforeunload` dialog is dismissed too, so navigation is not held up. This is why a test that clicks **Delete issue** on a page guarded by `confirm()` so often looks like it did nothing: the guard ran, the answer was cancel, and the issue is still on the board. The dialog was not ignored -- it was answered, just not the way the test author assumed. ## Registering a listener flips the contract Attach `page.on('dialog', ...)` and Playwright stops answering for you. It hands your callback a `Dialog` object and waits. If the callback never calls `dialog.accept()` or `dialog.dismiss()`, the dialog stays on screen, the page stays blocked, and the click that opened it never resolves -- the test fails on the action timeout with a message about the click, which is why this one is hard to diagnose from the error alone. The trap has three common shapes: an early `return` on some branch, a thrown assertion inside the callback that skips the answer, and a handler registered for a `confirm` that also receives an unexpected `alert` it has no branch for. Answer on every path. ## The Dialog object | member | what it gives you | | --- | --- | | `dialog.type()` | `alert`, `confirm`, `prompt` or `beforeunload` | | `dialog.message()` | the text shown to the user | | `dialog.defaultValue()` | a prompt's pre-filled value, empty otherwise | | `dialog.accept(promptText)` | answers OK, optionally supplying prompt text | | `dialog.dismiss()` | answers Cancel | | `dialog.page()` | the page that opened it | ## Ordering: listen first, act second 1. Register the handler -- `page.once('dialog', ...)` for a dialog you expect exactly one of. 2. Perform the action that opens it. 3. Await that action, then assert. The event fires *during* the click, not after it, so a handler registered on the line below the awaited click is registered too late. `page.once` is usually the right choice over `page.on` inside a test: it answers the dialog you are testing and then gets out of the way, instead of silently absorbing a second, unexpected dialog later in the same test. ## Assert, do not merely answer Answering a dialog proves nothing about the application. The interesting facts are that a dialog appeared at all, what it said, and whether the page honoured the answer. Capture `dialog.message()` into a variable inside the handler and assert on it after the action rather than asserting inside the callback -- an assertion that throws inside the handler leaves the dialog unanswered and turns a clear failure into a timeout. For a `prompt`, `dialog.accept('Cannot reproduce')` resolves it with that string while `dialog.dismiss()` makes it return `null`, so the two branches of the application's logic are both reachable from a test. ## Putting it together on an issue tracker Deleting an issue behind a confirmation is the canonical case. With no handler, the delete never happens and an assertion on the empty board fails for a reason the error message does not explain. With `page.once('dialog', d => d.accept())` registered before the click, the delete proceeds and the test is meaningful. With a handler that inspects but never answers, the click times out. All three outcomes come from the same page and the same click -- the difference is entirely in how the test set up its listener.

  • How do you supply text to a prompt dialog?
    Pass it to accept: `dialog.accept('Cannot reproduce')` resolves the prompt with that string, while `dialog.dismiss()` makes the page receive null. `dialog.defaultValue()` reads whatever the page pre-filled, which is often worth asserting before you answer so a changed default does not go unnoticed.
  • A handler is registered but the click still times out -- what would you check?
    Whether the handler answers on every path. An early return, a thrown assertion inside the callback, or a branch written for a confirm that receives an unexpected alert all leave the dialog open. The page stays blocked, and the failure is reported against the click rather than the dialog.

saying these in an interview costs you the question

  • Says an unhandled dialog always freezes a Playwright test
  • Registers the dialog handler after the click that opens it
  • Asserts inside the handler and never accepts or dismisses
  • Believes confirm returns true when nothing handles it
  • Thinks a dialog listener can drive the native file picker