Playwright Python: Handle Alerts, Confirm and Prompt Dialogs, iFrames, Popups and New Tabs

Real applications do not stay on one page. They show alert and confirm dialogs, embed content in iframes, and open links in new tabs. Each needs a slightly different technique in Playwright. In this lesson you will handle all three kinds of JavaScript dialogs, find elements inside iframes (including the lazy-loaded iframe on the SQA Tools practice page), and open, switch between and close tabs and popups.

Advertisement

📖 What You’ll Learn: how Playwright treats dialogs by default, page.on and page.once dialog handlers, dialog.type, message, default_value, accept and dismiss, answering a prompt, frame_locator and content_frame, nested frames, the Frame API, lazy-loaded iframes, expect_popup and context.expect_page, listing and switching tabs, and closing pages.


💬 JavaScript Dialogs: alert, confirm and prompt

The practice page has three buttons that open the three kinds of native browser dialogs:

ButtonJavaScriptDialog typeUser can
Simple Alertalert("This is a simple alert!")alertClick OK
Confirm Alertconfirm("Do you want to continue?")confirmOK or Cancel
Prompt Alertprompt("Enter your name:")promptType text, then OK or Cancel

The default: dialogs are dismissed automatically

If your test has no dialog handler, Playwright automatically dismisses every dialog, so the test never hangs. For an alert that is harmless; for a confirm it means “Cancel”. The first test proves that clicking “Simple Alert” without a handler does not block anything.

Handling dialogs with page.on / page.once

To inspect or answer a dialog, register a handler before the action that opens it:

📄 tests/conftest.py

import pytest
from playwright.sync_api import Page


@pytest.fixture
def practice(page: Page) -> Page:
    page.goto("https://sqatools.in/automation-practice-page/")
    return page

📄 tests/test_dialogs.py

from playwright.sync_api import Dialog, Page, expect


def test_alert_without_handler_is_dismissed(practice: Page):
    practice.get_by_role("button", name="Simple Alert").click()
    # The test continues: Playwright dismissed the alert automatically
    expect(practice.get_by_role("button", name="Simple Alert")).to_be_visible()


def test_read_and_accept_alert(practice: Page):
    messages = []

    def handle(dialog: Dialog):
        messages.append((dialog.type, dialog.message))
        dialog.accept()

    practice.once("dialog", handle)                       # handle only the next dialog
    practice.get_by_role("button", name="Simple Alert").click()
    print("\nalert:", messages)
    assert messages == [("alert", "This is a simple alert!")]


def test_confirm_ok_and_cancel(practice: Page):
    seen = []
    practice.once("dialog", lambda d: (seen.append(d.message), d.accept()))    # OK
    practice.get_by_role("button", name="Confirm Alert").click()
    practice.once("dialog", lambda d: (seen.append(d.type), d.dismiss()))      # Cancel
    practice.get_by_role("button", name="Confirm Alert").click()
    assert seen == ["Do you want to continue?", "confirm"]


def test_answer_prompt(practice: Page):
    answer = {}

    def handle(dialog: Dialog):
        answer["type"] = dialog.type
        answer["default"] = dialog.default_value
        dialog.accept("Deepesh")                           # type text and press OK

    practice.once("dialog", handle)
    # The page discards the answer, so we call prompt() ourselves to see what it returns
    result = practice.evaluate("() => prompt('Enter your name:')")
    print("\nprompt:", answer, "->", result)
    assert result == "Deepesh"

Line by line:

  • page.once("dialog", handler) calls the handler for the next dialog only; page.on("dialog", handler) calls it for every dialog until you remove it with page.remove_listener("dialog", handler).
  • The handler receives a Dialog object: dialog.type (“alert”, “confirm”, “prompt” or “beforeunload”), dialog.message, and for prompts dialog.default_value.
  • dialog.accept() clicks OK; dialog.accept("text") also types the answer into a prompt; dialog.dismiss() clicks Cancel.
  • Your handler must call accept() or dismiss(). If it does neither, the dialog stays open and the page is stuck, so the click never completes.
  • The practice page’s showPrompt() throws the answer away, so the last test calls prompt() through page.evaluate() to show the value the page would receive.

🖥️ Output

alert: [('alert', 'This is a simple alert!')]
prompt: {'type': 'prompt', 'default': ''} -> Deepesh

🪟 iFrames

An iframe embeds a separate HTML document inside the page. Elements inside it are invisible to normal page locators; you must enter the frame first with frame_locator() (or content_frame on a locator pointing at the iframe).

A self-contained example

page.set_content() with the srcdoc attribute builds a page with an iframe, so the example works offline and never changes:

📄 tests/test_frames.py

from playwright.sync_api import Page, expect

HTML = """
<h1>Main page</h1>
<iframe id="payment" srcdoc="
    <h2>Card details</h2>
    <label>Card number <input id='card'></label>
    <iframe id='inner' srcdoc='<button>Pay now</button>'></iframe>
"></iframe>
"""


def test_frame_locator(page: Page):
    page.set_content(HTML)

    # A normal page locator cannot see inside the iframe
    expect(page.locator("#card")).to_have_count(0)

    # frame_locator() enters the iframe; then use any locator inside it
    payment = page.frame_locator("#payment")
    payment.get_by_label("Card number").fill("4111 1111 1111 1111")
    expect(payment.get_by_label("Card number")).to_have_value("4111 1111 1111 1111")

    # Nested iframe: chain frame_locator calls
    expect(payment.frame_locator("#inner").get_by_role("button", name="Pay now")).to_be_visible()

    # content_frame turns a locator for the <iframe> element into a frame locator
    expect(page.locator("#payment").content_frame.get_by_role("heading")).to_have_text("Card details")
  • page.frame_locator("#payment") takes a selector for the <iframe> element and returns a FrameLocator. Locators created from it search inside that frame.
  • Frames inside frames are reached by chaining frame_locator() again.
  • locator("#payment").content_frame does the same from a normal locator, which is useful when you already have one.
  • Auto-waiting works inside frames too: Playwright waits for the frame and the element to be ready.

The practice page’s lazy-loaded iframe

The iframe on the practice page is lazy-loaded: a performance plugin on the site starts it as about:blank and only loads the real page (https://example.com) when the iframe scrolls into view. A test must scroll to it first:

📄 tests/test_frames.py

def test_practice_page_iframe(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    iframe = page.locator("#sampleIframe")

    iframe.scroll_into_view_if_needed()                 # triggers the lazy loading
    expect(iframe).to_have_attribute("src", "https://example.com")

    frame = page.frame_locator("#sampleIframe")
    expect(frame.get_by_text("This domain is for use in documentation examples")).to_be_visible()


def test_frame_objects(page: Page):
    page.set_content(HTML)
    names = [f.name for f in page.frames]
    print("\nframes:", names)
    payment = page.frame(name="payment")
    assert payment is not None and payment.parent_frame == page.main_frame
  • The lazy loading is real-world behaviour worth knowing: if an element inside an iframe “never appears”, check whether the iframe has loaded at all (src and the network tab in the Trace Viewer).
  • page.frames lists every frame, starting with the main frame. page.frame(name=...) or page.frame(url=...) returns a Frame object, an older, lower-level API. Prefer frame_locator() for test steps, because it auto-waits and re-resolves the frame.

🗂️ Popups and New Tabs

A link with target="_blank" or a window.open() call opens a new page. Start waiting for it before the click with page.expect_popup():

📄 tests/test_tabs.py

import re

from playwright.sync_api import BrowserContext, Page, expect


def test_new_tab_with_expect_popup(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")

    with page.expect_popup() as popup_info:
        page.get_by_role("link", name="Dummy Page").click()
    new_tab = popup_info.value
    new_tab.wait_for_load_state()

    expect(new_tab).to_have_url(re.compile(r"dummy-booking-website"))
    print("\nnew tab title:", new_tab.title())

    # Switch back and forth simply by using the right Page object
    expect(page).to_have_url("https://sqatools.in/automation-practice-page/")
    new_tab.close()
    expect(page.get_by_role("link", name="Dummy Page")).to_be_visible()


def test_tabs_in_context(page: Page, context: BrowserContext):
    page.goto("https://sqatools.in/automation-practice-page/")

    with context.expect_page() as new_page_info:
        page.get_by_role("link", name="Login Page").first.click()
    login_tab = new_page_info.value

    login_tab.wait_for_load_state()
    print("\npages open:", len(context.pages), [p.url for p in context.pages])
    expect(login_tab.get_by_role("button", name="Log in")).to_be_visible()

    login_tab.bring_to_front()                         # make it the active tab (visible in headed mode)
    login_tab.close()
    assert len(context.pages) == 1
  • page.expect_popup() waits for a popup opened by that page; context.expect_page() waits for any new page in the context. Both return the new Page in .value.
  • wait_for_load_state() waits until the new tab has loaded (the load event by default).
  • There is no “switch to” command: each tab is its own Page object, and you simply call methods on the one you want. bring_to_front() only changes which tab is visible in headed mode.
  • context.pages lists all open pages in the context; closing a page removes it.
  • The practice page has two “Login Page” links, so .first picks the one at the top (strictness, Lesson 31).

⌨️ Terminal

pytest -q -s tests

🖥️ Output

.
alert: [('alert', 'This is a simple alert!')]
..
prompt: {'type': 'prompt', 'default': ''} -> Deepesh
...
frames: ['', 'payment', 'inner']
.
new tab title: Dummy Booking Website - SQA Tools
.
pages open: 2 ['https://sqatools.in/automation-practice-page/', 'https://sqatools.in/login-page/']
.
9 passed, 1 warning in 17.59s

The main frame has an empty name (''); the two iframes are named after their id attributes.


❓ Frequently Asked Questions

🤔 My test hangs after clicking a button that opens an alert. Why?

Your dialog handler neither accepted nor dismissed the dialog. Every handler must call dialog.accept() or dialog.dismiss(). Without any handler, Playwright dismisses dialogs for you.

🤔 Why can’t Playwright find an element that I can see on screen?

It is probably inside an iframe. Use page.frame_locator(...) (or content_frame) to enter the frame first, and check whether the iframe has loaded.

🤔 How do I handle a “Leave site?” (beforeunload) dialog?

It arrives as a dialog with type == "beforeunload", but only when you close the page with page.close(run_before_unload=True). Handle it like any other dialog.

🤔 Can I test new tabs in Firefox and WebKit too?

Yes. Popups, tabs, dialogs and frames work the same in all three engines; run with --browser firefox --browser webkit.


🏁 Summary

  • Without a handler, Playwright dismisses dialogs; register page.once("dialog", ...) or page.on(...) before the action to read, accept (optionally with prompt text) or dismiss them.
  • Use frame_locator() (or content_frame) to work inside iframes, chain it for nested frames, and remember that iframes may be lazy-loaded.
  • Wrap clicks that open tabs in page.expect_popup() or context.expect_page(); each tab is a separate Page object.

Next, you will read and verify data in web tables.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

Leave a Comment