Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Playwright›Handling popups, dialogs, and alerts

Core Playwright Concepts

Handling popups, dialogs, and alerts

Popups, dialogs, and alerts are common UI elements that interrupt normal page flow, requiring explicit handling in automated tests. They matter because failing to manage them can cause tests to hang or fail unexpectedly, breaking your test suite. You reach for these techniques whenever your application uses JavaScript alerts, confirmation dialogs, or new browser windows/tabs.

Understanding the Three Types of Interruptions

Playwright categorizes interruptions into three distinct types: alerts, confirms, and prompts (collectively called 'dialogs'), plus separate 'popups' which are new pages or tabs. Dialogs are modal and block further interaction until dismissed, while popups are independent browsing contexts. The key insight is that dialogs are synchronous from the page's perspective—they pause JavaScript execution—while popups are asynchronous. This distinction explains why dialogs require event-based handling (you must listen before they appear), whereas popups can be awaited after they're triggered. Recognizing which type you're dealing with determines the correct handling strategy. For example, a login form that opens in a new tab is a popup, while a 'Delete item?' confirmation is a dialog. Both interrupt your test flow but in fundamentally different ways.

# Example showing the three dialog types and a popup
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  // Dialog types demonstration
  await page.setContent(`
    <button onclick="alert('Alert message')">Show Alert</button>
    <button onclick="confirm('Confirm action?')">Show Confirm</button>
    <button onclick="prompt('Enter name:', 'Default')">Show Prompt</button>
    <button onclick="window.open('about:blank', '_blank')">Open Popup</button>
  `);
  
  // Dialogs must be handled before they appear
  page.on('dialog', dialog => {
    console.log(`Dialog type: ${dialog.type()}, message: ${dialog.message()}`);
    dialog.dismiss(); // Default action for demonstration
  });
  
  // Popups are handled after they appear
  const [popup] = await Promise.all([
    page.waitForEvent('popup'),
    page.click('text=Open Popup')
  ]);
  
  console.log(`Popup opened with URL: ${popup.url()}`);
  await browser.close();
})();

Handling Dialogs Before They Appear

Dialogs in Playwright must be handled before they are triggered because they block the page's execution. This is why you register dialog handlers using page.on('dialog') rather than waiting for them to appear. The handler receives a Dialog object containing the type (alert/confirm/prompt), message, and default value (for prompts). You must either accept() or dismiss() the dialog within the handler, otherwise Playwright will throw an error after a timeout. This design mirrors how real users interact with dialogs—they must respond before continuing. The critical insight is that dialog handlers are persistent; once registered, they handle all subsequent dialogs of that type until removed. This explains why you might see unexpected behavior if you register multiple handlers for the same dialog type—the last one registered will take precedence.

# Complete dialog handling example
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  await page.setContent(`
    <button onclick="confirm('Proceed with deletion?')">Delete Item</button>
  `);
  
  // Handler must be registered before the dialog appears
  page.on('dialog', async dialog => {
    console.log(`Dialog message: ${dialog.message()}`);
    
    // Different handling based on dialog type
    if (dialog.type() === 'confirm') {
      // Accept the confirmation (click OK)
      await dialog.accept();
      console.log('Confirmed deletion');
    } else {
      // Dismiss all other dialog types
      await dialog.dismiss();
    }
  });
  
  // Trigger the dialog
  await page.click('text=Delete Item');
  console.log('Test completed successfully');
  
  await browser.close();
})();

Working with Prompts and Input Values

Prompts are dialogs that require user input, and they present unique challenges because they combine the blocking behavior of dialogs with the need to provide dynamic values. The Dialog object provides a defaultValue() method to read the pre-filled value and an accept(value) method that takes the user's input. This design allows you to simulate real user behavior by either accepting the default value or providing custom input. The key insight is that prompt handling requires two decisions: whether to accept or dismiss, and what value to provide if accepting. This explains why prompt handlers often contain more logic than other dialog handlers. For example, you might want to test both valid and invalid inputs in the same test case. Remember that dismissing a prompt returns null to the page, which your application should handle gracefully.

# Prompt handling with dynamic input
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  await page.setContent(`
    <button onclick="const name = prompt('Enter your name:', 'Guest'); alert('Hello ' + name)">Greet</button>
  `);
  
  // Handle the prompt first
  page.on('dialog', async dialog => {
    if (dialog.type() === 'prompt') {
      console.log(`Prompt default value: ${dialog.defaultValue()}`);
      // Provide custom input
      await dialog.accept('Playwright User');
    } else if (dialog.type() === 'alert') {
      // Handle the subsequent alert
      console.log(`Alert message: ${dialog.message()}`);
      await dialog.accept();
    }
  });
  
  // Trigger the prompt
  await page.click('text=Greet');
  console.log('Prompt test completed');
  
  await browser.close();
})();

Managing New Browser Windows and Tabs

Popups in Playwright refer to new browser contexts—windows or tabs—that open during test execution. Unlike dialogs, popups are asynchronous and don't block the main page. The key insight is that popups are separate Page objects with their own lifecycle, requiring explicit handling. Playwright provides the page.waitForEvent('popup') method to await the new page object, which you must use before interacting with the popup. This design allows you to handle both user-initiated popups (like clicking a 'Open in new tab' link) and JavaScript-initiated popups (window.open()). The critical difference from dialogs is that popups persist until explicitly closed, allowing you to perform multiple actions on them. This explains why popup handling often involves switching between the original page and the popup context. Remember that popups inherit cookies and storage from the parent page unless they're created with noopener, which affects authentication state.

# Complete popup handling example
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  await page.goto('https://example.com');
  
  // Set up popup handler before triggering the popup
  const popupPromise = page.waitForEvent('popup');
  
  // Trigger popup (could be from a link or window.open())
  await page.evaluate(() => window.open('https://example.org'));
  
  // Wait for and interact with the popup
  const popup = await popupPromise;
  console.log(`Popup opened with URL: ${popup.url()}`);
  
  // Perform actions on the popup
  await popup.fill('input[name="q"]', 'Playwright testing');
  await popup.click('input[type="submit"]');
  
  // Switch back to main page if needed
  console.log(`Main page URL: ${page.url()}`);
  
  await browser.close();
})();

Advanced: Combining Dialogs and Popups in Complex Flows

Real-world applications often combine dialogs and popups in complex user flows, requiring sophisticated handling strategies. The key insight is that these interruptions can occur in any order and may even be nested (a popup that triggers a dialog). Playwright's event-based architecture handles this naturally—you can register multiple handlers that will respond to events as they occur. The critical pattern is to set up all handlers before triggering any actions, then use Promise.all() to coordinate multiple asynchronous operations. This explains why complex flows often use event listeners for both dialogs and popups simultaneously. For example, a 'Share' button might open a popup with a form that includes a confirmation dialog. The test must handle both the popup opening and the subsequent dialog. Remember that handlers remain active until the page is closed, so you may need to remove them explicitly if they should only handle specific events.

# Complex flow with both dialogs and popups
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  await page.setContent(`
    <button onclick="window.open('about:blank', '_blank')">Open Popup</button>
    <button onclick="confirm('Proceed with action?')">Show Confirm</button>
  `);
  
  // Set up all handlers before any actions
  const dialogHandler = dialog => {
    console.log(`Dialog handled: ${dialog.message()}`);
    return dialog.accept();
  };
  page.on('dialog', dialogHandler);
  
  // Handle popup and dialog in parallel
  const [popup] = await Promise.all([
    page.waitForEvent('popup'),
    page.click('text=Open Popup')
  ]);
  
  // Now trigger a dialog while popup is open
  await page.click('text=Show Confirm');
  
  // Interact with the popup
  await popup.goto('https://example.com');
  console.log(`Popup title: ${await popup.title()}`);
  
  // Clean up
  page.off('dialog', dialogHandler);
  await browser.close();
})();

Key points

  • Dialogs (alerts, confirms, prompts) must be handled before they appear because they block page execution, while popups are new pages that can be handled after creation.
  • The page.on('dialog') handler receives a Dialog object containing the type, message, and default value, which you must either accept() or dismiss() to continue execution.
  • Prompts require special handling because they combine dialog blocking behavior with the need to provide input values through the accept(value) method.
  • Popups are separate Page objects that persist until closed, allowing multiple interactions, and are handled using page.waitForEvent('popup').
  • Multiple handlers can be registered for different event types, and they remain active until explicitly removed or the page is closed.
  • Complex flows often require setting up all handlers before triggering any actions, using Promise.all() to coordinate asynchronous operations.
  • Dialog handlers are persistent and will handle all subsequent dialogs of the same type until removed, which can lead to unexpected behavior if not managed properly.
  • Popups inherit cookies and storage from the parent page unless created with noopener, which affects authentication state and test isolation.

Common mistakes

  • Mistake: Not waiting for the popup/dialog to appear before interacting with it. Why it's wrong: Playwright executes commands asynchronously, so the popup might not be ready when the test tries to interact with it. Fix: Use `page.waitForEvent('dialog')` or `page.on('dialog', ...)` to ensure the dialog is present before acting on it.
  • Mistake: Assuming all dialogs are browser-native alerts. Why it's wrong: Custom modal dialogs (e.g., HTML/CSS-based) require different handling than native alerts, confirms, or prompts. Fix: Identify whether the dialog is native or custom and use the appropriate method (e.g., `page.locator()` for custom dialogs).
  • Mistake: Forgetting to call `dialog.accept()` or `dialog.dismiss()`. Why it's wrong: If the dialog handler doesn’t explicitly accept or dismiss the dialog, the test will hang indefinitely. Fix: Always include `dialog.accept()` or `dialog.dismiss()` in the handler.
  • Mistake: Using `page.click()` without handling the resulting dialog. Why it's wrong: If a click triggers a dialog, the test will fail unless the dialog is handled first. Fix: Register a dialog handler *before* triggering the action that opens the dialog.
  • Mistake: Hardcoding dialog message text in assertions. Why it's wrong: Dialog messages might change or be localized, causing flaky tests. Fix: Use partial matching or dynamic assertions (e.g., `expect(dialog.message()).toContain('expected text')`).

Interview questions

How do you handle a simple alert in Playwright?

In Playwright, handling a simple alert is straightforward using the `page.on('dialog')` event listener. This event fires whenever a dialog, like an alert, confirm, or prompt, appears. You listen for the event, then call `dialog.accept()` to close the alert. Here’s why this works: alerts are modal and block further execution, so Playwright provides this event-based approach to interact with them asynchronously. For example: `page.on('dialog', dialog => dialog.accept());` ensures the alert is automatically accepted without manual intervention. This is essential for automated testing, where you can’t click 'OK' manually.

What’s the difference between `dialog.accept()` and `dialog.dismiss()` in Playwright?

The difference between `dialog.accept()` and `dialog.dismiss()` lies in how they handle dialogs like confirms or prompts. `dialog.accept()` simulates clicking 'OK' or 'Yes', which is used when you want to proceed with the action that triggered the dialog. For example, in a confirm dialog, `accept()` returns `true` to the JavaScript handler. On the other hand, `dialog.dismiss()` simulates clicking 'Cancel' or closing the dialog, which returns `false` or `null` to the handler. This is critical for testing user flows where the outcome depends on the dialog response. For instance, `page.on('dialog', dialog => dialog.dismiss());` would cancel a form submission if a confirm dialog appears.

How do you handle a prompt dialog where you need to enter text before accepting it?

Handling a prompt dialog in Playwright requires using the `dialog` object’s `defaultValue()` and `accept(text)` methods. A prompt dialog asks the user for input, so you first check if the dialog type is 'prompt', then pass the desired text to `accept()`. For example: `page.on('dialog', dialog => { if (dialog.type() === 'prompt') dialog.accept('Test Input'); });` This works because `accept(text)` not only closes the dialog but also sends the provided text to the page’s JavaScript handler. Without this, the prompt would block execution, and the test would hang. This approach is essential for testing forms or features that rely on user-provided input via prompts.

Compare the event-based approach for dialogs with the `page.waitForEvent('dialog')` method. When would you use each?

The event-based approach using `page.on('dialog')` and `page.waitForEvent('dialog')` both handle dialogs, but they serve different purposes. The event-based approach is ideal for scenarios where you expect a dialog to appear at some point during a test but don’t know exactly when. For example, you might set up the listener at the start of a test to handle any unexpected alerts. In contrast, `page.waitForEvent('dialog')` is used when you know a dialog will appear after a specific action, like clicking a button. It waits synchronously for the dialog to appear, making it easier to chain with other actions. For instance: `const dialog = await page.waitForEvent('dialog'); await dialog.accept();` ensures the test pauses until the dialog is handled, which is cleaner for predictable flows.

How do you test that a dialog was triggered after a specific action, like clicking a button?

To test that a dialog was triggered after a specific action, you combine `page.waitForEvent('dialog')` with the action that triggers the dialog. This ensures the test verifies both the action and the dialog’s appearance. For example, if clicking a button should show an alert, you’d write: `const [dialog] = await Promise.all([ page.waitForEvent('dialog'), page.click('#alertButton') ]); await dialog.accept();` This works because `Promise.all` waits for both the click and the dialog event to resolve, ensuring the dialog appears as expected. Without this, the test might miss the dialog or fail if the dialog doesn’t appear. This approach is critical for validating that UI interactions behave as intended.

How do you handle a scenario where a dialog might or might not appear, and you need to proceed regardless?

Handling a dialog that might or might not appear requires a combination of the event-based approach and a timeout to avoid hanging the test. You set up a `page.on('dialog')` listener at the start of the test to handle the dialog if it appears, but you also wrap the action in a `Promise.race` with a timeout to proceed if no dialog appears. For example: `page.on('dialog', dialog => dialog.accept()); await Promise.race([ page.click('#maybeAlertButton'), new Promise(resolve => setTimeout(resolve, 2000)) ]);` This ensures the test doesn’t wait indefinitely for a dialog that might not come. The timeout value should be short enough to avoid slowing down the test but long enough to account for delays. This approach is essential for robust testing of unpredictable UI behaviors.

All Playwright interview questions →

Check yourself

1. What is the correct way to handle a browser-native alert that appears after clicking a button?

  • A.Use `page.locator('button').click()` followed by `page.acceptAlert()`.
  • B.Register a `page.on('dialog', ...)` handler *before* clicking the button, then call `dialog.accept()`.
  • C.Use `page.waitForSelector('alert')` to wait for the alert, then interact with it.
  • D.Call `page.evaluate(() => alert('handled'))` to override the alert.
Show answer

B. Register a `page.on('dialog', ...)` handler *before* clicking the button, then call `dialog.accept()`.
The right answer registers a dialog handler *before* the action that triggers the alert, ensuring the dialog is captured. Option 1 is wrong because `acceptAlert()` doesn’t exist. Option 3 is wrong because native alerts aren’t DOM elements. Option 4 is wrong because it doesn’t handle the actual alert.

2. How do you test that a custom modal (e.g., a `<div>` styled as a dialog) is visible after an action?

  • A.Use `page.on('dialog', ...)` to listen for the modal.
  • B.Use `page.waitForSelector('.modal', { state: 'visible' })` after triggering the action.
  • C.Call `page.isVisible('.modal')` immediately after the action.
  • D.Use `page.evaluate(() => document.querySelector('.modal').showModal())`.
Show answer

B. Use `page.waitForSelector('.modal', { state: 'visible' })` after triggering the action.
The right answer waits for the modal to become visible, accounting for potential delays. Option 0 is wrong because custom modals aren’t native dialogs. Option 2 is wrong because it doesn’t wait for visibility. Option 3 is wrong because it manually triggers the modal instead of testing the actual behavior.

3. What happens if you forget to call `dialog.accept()` or `dialog.dismiss()` in a dialog handler?

  • A.The test passes, but the dialog remains open in the browser.
  • B.The test hangs indefinitely because Playwright waits for the dialog to be handled.
  • C.Playwright automatically dismisses the dialog after a timeout.
  • D.The test fails with an error about an unhandled dialog.
Show answer

B. The test hangs indefinitely because Playwright waits for the dialog to be handled.
The right answer is that the test hangs because Playwright waits for the dialog to be explicitly handled. Option 0 is wrong because the test doesn’t pass. Option 2 is wrong because Playwright doesn’t auto-dismiss. Option 3 is wrong because the error occurs only if no handler is registered at all.

4. How do you verify the text of a native confirm dialog before accepting it?

  • A.Use `page.textContent('body')` to read the dialog text.
  • B.Access `dialog.message()` in the handler and assert on it before calling `dialog.accept()`.
  • C.Use `page.screenshot()` to capture the dialog and OCR the text.
  • D.Call `page.evaluate(() => confirm('expected text'))` to override the message.
Show answer

B. Access `dialog.message()` in the handler and assert on it before calling `dialog.accept()`.
The right answer uses `dialog.message()` to inspect the text. Option 0 is wrong because native dialogs aren’t part of the DOM. Option 2 is wrong because it’s overcomplicating the solution. Option 3 is wrong because it doesn’t verify the actual dialog text.

5. Why might a test fail if you register a dialog handler *after* triggering the action that opens the dialog?

  • A.The handler is registered too late and misses the dialog event.
  • B.Playwright requires handlers to be registered in a specific order.
  • C.The dialog is automatically dismissed before the handler is registered.
  • D.The test will pass, but the dialog handling is slower.
Show answer

A. The handler is registered too late and misses the dialog event.
The right answer is that the handler might miss the dialog event if registered too late. Option 1 is wrong because order isn’t the issue—timing is. Option 2 is wrong because Playwright doesn’t auto-dismiss. Option 3 is wrong because the test fails, not just slows down.

Take the full Playwright quiz →

← PreviousWorking with frames and iframesNext →Page navigation and history management

Playwright

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app