Playwright's onceDialog Saved Me From a Handler Leak — Until It Didn't

Table of Contents
I registered page.onDialog() to accept a delete confirmation in a test. The delete worked fine. Then, three steps later in the same test, the user clicked “Logout” — which also triggers a confirm() dialog. My delete handler was still registered. It silently accepted the logout confirmation, the user got logged out mid-test, and the next assertion failed with a session error. I spent 20 minutes blaming the app before realizing my own handler was the problem.
The Problem: onDialog Is Permanent
page.onDialog() registers a listener that stays active for the entire lifetime of the page. Every dialog that fires — whether you intended to handle it or not — goes through that handler.
The bug looks like an application failure
// This handler stays registered for ALL future dialogs on this pagepage.onDialog(dialog -> dialog.accept());
// Delete confirmation — handled correctlypage.locator("[data-testid='delete-patient']").click();
// ... 3 steps later, logout also triggers confirm() ...page.locator("[data-testid='logout']").click();// Logout confirm silently accepted — NOT what we wantedThe logout dialog should have been dismissed (cancel the logout, stay in the app), but the permanent handler accepted it. The test continued on a logged-out session and failed on the next page interaction.
The Fix: onceDialog Auto-Removes
page.onceDialog() fires exactly once, then removes itself. The dialog handler is scoped to the next dialog only — after that, it’s gone.
Scope the listener to the next destructive action
// This handler fires ONCE, then auto-removespage.onceDialog(dialog -> dialog.accept());
// Delete confirmation — handled, handler now gonepage.locator("[data-testid='delete-patient']").click();
// Logout triggers confirm() — no active handler// Falls through to default behavior (or your base class handler)page.locator("[data-testid='logout']").click();Now the delete confirmation gets accepted, the handler removes itself, and the logout dialog falls through to whatever default handler you’ve registered in your base test class.
When to Use Which
That same “scope the state to the action” rule shows up in thread-safe parallel test execution: local setup is easier to reason about than global cleanup after the fact.
The rule of thumb: if you’re writing page.onDialog() inside a test method for a dialog you know will fire, page.onceDialog() is the cleaner choice. For dialogs that might appear, use onDialog + offDialog with a finally block — here’s why.
