Halmurat T.
Halmurat T.

Senior SDET

Home Blog Books ask About

The Dispatch

Weekly QA notes from the trenches.

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

© 2026 Halmurat T.

Automation 24
  • Selenium
  • Playwright
  • Appium
  • Cypress
AI Testing 5
CI/CD 6
  • GitHub Actions
  • Slack Reporting
QA Strategy 4
Case Studies 5
Blog/Automation
AutomationHalmurat T./November 4, 2025/3 min

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

Filed underplaywright/framework-design/design-patterns
Playwright's onceDialog Saved Me From a Handler Leak — Until It Didn't

Table of Contents
  • The Problem: onDialog Is Permanent
  • The bug looks like an application failure
  • The Fix: onceDialog Auto-Removes
  • Scope the listener to the next destructive action
  • When to Use Which

On this page

  • The Problem: onDialog Is Permanent
  • The bug looks like an application failure
  • The Fix: onceDialog Auto-Removes
  • Scope the listener to the next destructive action
  • When to Use Which

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

src/test/java/tests/PatientTest.java
// This handler stays registered for ALL future dialogs on this page
page.onDialog(dialog -> dialog.accept());
// Delete confirmation — handled correctly
page.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 wanted

The 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

src/test/java/tests/PatientTest.java
// This handler fires ONCE, then auto-removes
page.onceDialog(dialog -> dialog.accept());
// Delete confirmation — handled, handler now gone
page.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

[ TIP ]

onDialog — Global default handlers. Register once in your base test class to catch unexpected dialogs across the entire test. This is your safety net for session timeouts and surprise alerts.

onceDialog — Action-specific handling. Use when you’re about to trigger a specific dialog and want to handle just that one. Delete confirmations, form submissions, destructive actions — anywhere the handler should fire once and disappear.

[ WARNING ]

Gotcha: onceDialog only auto-removes when the dialog actually fires. If you register it as a guard for a dialog that might appear and it never does, the handler stays registered — and silently eats the next dialog from a completely different action. For guard/safety-net patterns where the dialog isn’t guaranteed, use onDialog + offDialog + finally instead. I ran into this exact bug in production and wrote up the dangling handler problem.

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.

§ Further Reading 03 of 03
01Automation

Migrating Off Cypress? Here's When to Keep It

An honest take on Cypress vs Playwright migrations from an SDET who's done three, including the signals that tell you not to migrate your suite just yet.

Read →
02Automation

XPath text() vs Dot — Why Your Text Match Fails

The real difference between XPath text(), dot, contains(), and normalize-space() for test automation — with examples that explain real flaky failures.

Read →
03Automation

The Async Trap Behind Flaky Playwright Tests

The 3 async mistakes that cause flaky Playwright tests after a Selenium migration, and how we fixed a 23% intermittent failure rate in our CI pipeline.

Read →

Don't miss a thing

Subscribe to get updates straight to your inbox.

HT

No spam · Unsubscribe anytime

Welcome aboard!

You're on the list. Expect real-world QA insights — no fluff, no spam.

§ Colophon

Halmurat T. — Senior SDET writing about test automation, CI/CD, and QA strategy from 10+ years in the enterprise trenches.

Set in
IBM Plex Sans, Lora, and IBM Plex Mono.
Built with
Astro, MDX, Tailwind CSS & Expressive Code. Served by Vercel.
Privacy
No cookies. No tracking scripts on the main thread — analytics run sandboxed via Partytown.
Source
github.com/Halmurat-Uyghur
Terminal
Try /ask to query Halmurat's notes in a shell prompt.

© 2026 Halmurat T. · Written in plain text, shipped in plain time.

Search
Esc

Search is not available in dev mode.

Run npm run build then npm run preview:local to test search locally.