Handling Modals, Alerts & Popups in Playwright

Handling Modals, Alerts & Popups in Playwright




Modals, dialogs, and popups are tiny UI pieces that cause big headaches in E2E automation. They introduce timing, focus, and context-switching problems. Today we’ll walk through robust Playwright patterns to handle:

    - native browser dialogs (`alert`, `confirm`, `prompt`)

    - DOM-based modals (Bootstrap, React, custom overlays)

    - popups opened with `window.open`

    - interacting with content inside iframes shown in modals.


1. Native browser dialogs (alert / confirm / prompt)

Native dialogs are triggered by `window.alert()`, `window.confirm()`, and `window.prompt()`. Playwright exposes them via its `Dialog` API.

Why they fail: if you don’t accept/dismiss the dialog, the page script can hang and tests time out.

Pattern (recommended):

page.once('dialog', async dialog => {

  console.log('Dialog type:', dialog.type()); // 'alert' | 'confirm' | 'prompt'

  console.log('Message:', dialog.message());

  await dialog.accept(); // or dialog.dismiss()

});

await page.click('text=Delete');

Prompt example:

page.once('dialog', d => d.accept('yes, please'));
await page.click('text=Enter promo code');

Notes:

  • Use page.once('dialog') to avoid leaking handlers across tests.
  • page.waitForEvent('dialog') is also handy when you need the dialog object returned.


2. DOM modals (React/Bootstrap/custom overlays)

Many apps render "modals" as part of the DOM instead of native dialogs. These require waiting for visibility, interacting with elements, and asserting the modal closed.

Common issues:

  • Modal overlay blocks clicks until animation finishes
  • Focus may be on a different element
  • Multiple overlays stacking (z-index)

Pattern:

const modal = page.getByRole('dialog', { name: /confirm/i }); // accessibility-first
await modal.waitFor({ state: 'visible' });
await modal.getByRole('button', { name: 'Confirm' }).click();
await expect(modal).toBeHidden();

Fallbacks & tricks:

  • If animations cause flakiness, wait for animationend or use await modal.waitFor({ state:'visible' }) then expect(modal).toHaveCSS('opacity', '1') if needed.
  • Prefer role selectors (getByRole) for accessibility and resilience.
  • Use keyboard fallback: await page.keyboard.press('Escape') to close modal when appropriate.


3. Popups (window.open)

When the app opens a new browser window/tab, you must capture that popup and interact with it.

Pattern:

const [popup] = await Promise.all([
  page.waitForEvent('popup'),
  page.click('text=Open Report')
]);
await popup.waitForLoadState('domcontentloaded');
await popup.getByText('Download').click();

Tips:

  • Use waitForEvent('popup') in the same Promise.all as the click for reliability.
  • popup behaves like a Page — use all page methods on it.


4. Iframes inside modals

If a modal contains an iframe (payment form, embedded widget), you need to switch context.

Pattern with frameLocator:

const frame = page.frameLocator('#modal iframe');
await frame.locator('button#submit').click();

Or by frame name:

const frame = page.frame({ name: 'payment' });
await frame.click('button#pay-now');

Tip: frameLocator avoids brittle waits and is the modern recommended way.


5. Practical test combining a few flows

test('handles modal with iframe and popup', async ({ page }) => {
  // mock API so modal content is predictable
  await page.route('**/api/get-modal', route => route.fulfill({ status: 200, body: JSON.stringify({ showModal: true }) }));

  // open page that will show modal
  await page.goto('https://example.com/dashboard');

  // wait for DOM modal
  const modal = page.getByRole('dialog', { name: /payment/i });
  await modal.waitFor({ state: 'visible' });

  // interact with iframe inside modal
  const frame = modal.frameLocator('iframe[name="pay"]');
  await frame.locator('input#card-number').fill('4111 1111 1111 1111');
  await frame.locator('button#pay').click();

  // popup for receipt
  const [receipt] = await Promise.all([
    page.waitForEvent('popup'),
    frame.locator('a#open-receipt').click()
  ]);
  await receipt.waitForLoadState();
  await expect(receipt).toHaveTitle(/Receipt/);
});

6. Pro tips & “surgery” for flaky modal tests

  • Use accessibility selectors (getByRole) — they’re stable and intent-driven.
  • Avoid force: true unless absolutely required — it hides real UI problems.
  • Close handlers: always remove dialog listeners after test or use page.once.
  • Trace: use Playwright tracing to capture failing runs and examine screenshot/DOM.
  • Fallbacks: if modal sometimes appears off-screen, use locator.scrollIntoViewIfNeeded().
  • Timeout tuning: increase timeout only when necessary — prefer waiting on a meaningful condition (selector state, network idle).

7. Common gotchas

  • Third-party widget modals (Stripe, PayPal) often live in cross-origin iframes — use test card numbers and test keys or mocks.
  • Recaptcha and bot detection: do not attempt to bypass; mock or stub the upstream verification in test environments.
  • Stale element errors: re-query locators after actions instead of reusing detached handles.

Final thoughts

Handling modals reliably short-circuits a large portion of flaky tests. With the right patterns — dialog handlers for native alerts, role-based selectors for DOM modals, waitForEvent('popup') for new windows, and frameLocator for iframe content — your tests will be far more deterministic and CI-friendly.


Follow @thebughacker and tag #30DaysOfPlaywrightAutomation if you used these patterns!



Previous Post Next Post

Contact Form