Should a Playwright suite auto-accept every dialog globally, or handle each one in its own test?
answer
- Dialogs guard destructive actions
- A global accept swallows the signal
- The default already prevents hangs
- Scope the handler to the expecting test
- One dialog takes exactly one answer
basics
~20 sHandle them per test. A blanket accept-everything handler hides the dialogs a test should be asserting on, and turns a real regression -- a confirmation that stops appearing, or one that names the wrong issue -- into a silent pass.
solid answer
~40 sA global `page.on('dialog', d => d.accept())` looks tidy but converts an assertion surface into background noise. Dialogs usually guard destructive work -- deleting an issue, discarding a draft comment -- and whether one appeared, what it said, and whether the page honoured the answer are exactly the behaviours worth testing. Blanket accept also inverts Playwright's safe default: with no listener at all, dialogs are auto-dismissed, so a stray `alert()` never hangs the run and an unexpected `confirm()` takes the cancel branch rather than the destructive one. The position I would defend is to keep the default, register a scoped `page.once('dialog', ...)` in the handful of tests that expect one, assert `dialog.message()`, and treat a dialog appearing where no test expected it as a signal rather than something to swallow.
code
typescript · 18 linesimport { test, expect } from '@playwright/test';
test('discarding a draft comment asks first', async ({ page }) => {
await page.goto('/issues/ISS-482');
const composer = page.getByRole('textbox', { name: 'Add a comment' });
await composer.fill('Reproduced on staging');
const asked: string[] = [];
page.once('dialog', async dialog => {
asked.push(dialog.message());
await dialog.dismiss();
});
await page.getByRole('button', { name: 'Discard' }).click();
expect(asked).toEqual(['Discard your unsaved comment?']);
await expect(composer).toHaveValue('Reproduced on staging');
});go deeper
Know that Playwright answers dialogs through a listener on the page, and that a test expecting a confirmation should register one itself rather than relying on a suite-wide default.
Explain the mechanics behind the policy: no listener means auto-dismiss, a listener owns the answer completely, and two listeners answering the same dialog is an error rather than harmless redundancy.
Argue from failure modes. Name the regressions a blanket accept hides -- a guard that vanished, a confirmation naming the wrong issue -- and describe how you would make an unexpected dialog visible.
Own the convention across the suite: the default policy, the narrow documented exceptions, and the mechanism that turns an unexpected dialog into a failing assertion rather than a swallowed one.
## What a blanket handler buys A single `page.on('dialog', d => d.accept())` applied to every test is tempting. It is one line, it removes a class of confusing timeouts, and it makes every destructive flow -- deleting an issue, discarding a draft comment, archiving a board -- proceed without per-test setup. On a suite that is already red for other reasons, it feels like progress. ## What it costs The cost is that dialogs stop being observable. A confirmation prompt is not incidental furniture; it is a product decision about protecting the user from an irreversible action. Once every dialog is accepted and none is asserted on, several real regressions become invisible: - The confirmation stops appearing at all, and the delete now happens on a single click. The test still passes. - The confirmation's wording changes to name the wrong issue. The test still passes. - A second, unexpected dialog appears -- a stale-session alert, a third-party widget -- and is swallowed along with the intended one. - A flow that should be guarded is shipped unguarded, and the suite that exists to catch exactly that says nothing. Blanket **accept** also inverts the framework's safe default. With no listener, Playwright dismisses dialogs, so an accidental `alert()` never hangs the run and a stray `confirm()` takes the cancel branch. Accepting everything means every unexpected confirmation is answered "yes" -- the most destructive possible default -- in tests running against a shared environment. ## Comparing the policies | policy | unexpected dialog | assertion value | main risk | | --- | --- | --- | --- | | no handler (default) | dismissed | none | expected confirmations silently cancel | | global accept | accepted | none | destructive answers, regressions hidden | | global recorder | dismissed and recorded | high | one extra listener to maintain | | per-test `page.once` | dismissed | high, where it matters | slightly more setup per test | ## A policy that scales 1. Keep the default for the vast majority of tests. Nothing hangs, and a dialog that was not expected is answered conservatively. 2. In the tests that genuinely expect one, register `page.once('dialog', ...)` before the action, capture `dialog.message()`, answer it deliberately, and assert the message afterwards. 3. Assert both branches where the product cares: accepting deletes the issue, dismissing leaves the draft comment intact. 4. If unexpected dialogs are a real concern in your application, add exactly one recording listener that collects messages and dismisses, then assert the collected list at the end of the test. Step 4 comes with a constraint worth knowing: a dialog takes exactly one answer. A second handler calling `accept()` or `dismiss()` on a dialog another listener already answered raises an error, so a global recorder and a per-test handler cannot both answer the same dialog. Pick one owner per dialog. ## Where a shared handler is defensible - A dialog that is genuinely incidental to everything under test -- a third-party component that alerts on every page load, for example -- and that no test is ever asserting on. - A migration window, where a suite is being brought back to green and the handler is scoped, commented with the specific dialog it exists for, and has a removal owner. Both cases share a property: someone wrote down *which* dialog is being absorbed. A handler that answers anything at all, forever, is the version that hides the regression. ## The framing to bring to the interview The question is not "how do I stop dialogs breaking my tests" -- the framework already answered that with its default. The question is "which dialogs is this suite responsible for asserting on, and how does an unexpected one become visible rather than silent". A blanket accept answers the first question and forfeits the second.
- How would you surface dialogs no test expected, without letting them hang the run?Register one recording listener that pushes `dialog.message()` and dismisses, then assert the collected list at the end -- empty for most tests, one known message for the few that expect a dialog. Keep it to a single owner: a second handler answering an already-answered dialog raises an error.
- When is a shared dialog handler genuinely the right call?When the dialog is incidental to everything under test -- a third-party widget alerting on every page, say -- and no test asserts on it. Scope it, comment which dialog it exists for, and give it a removal owner, so a new dialog is not absorbed by accident.
A blanket accept handler is the suite equivalent of clicking through every warning without reading it: nothing breaks today, and you learn nothing about what the application asked.
saying these in an interview costs you the question
- Accepts every dialog globally so tests never block
- Believes a missing handler makes tests hang
- Never asserts the message, only that the flow continued
- Adds a second handler for an already-answered dialog
- Treats an unexpected dialog as noise to suppress