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.
📖 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:
| Button | JavaScript | Dialog type | User can |
|---|---|---|---|
| Simple Alert | alert("This is a simple alert!") | alert | Click OK |
| Confirm Alert | confirm("Do you want to continue?") | confirm | OK or Cancel |
| Prompt Alert | prompt("Enter your name:") | prompt | Type 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 withpage.remove_listener("dialog", handler).- The handler receives a
Dialogobject:dialog.type(“alert”, “confirm”, “prompt” or “beforeunload”),dialog.message, and for promptsdialog.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()ordismiss(). 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 callsprompt()throughpage.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_framedoes 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 (
srcand the network tab in the Trace Viewer). page.frameslists every frame, starting with the main frame.page.frame(name=...)orpage.frame(url=...)returns aFrameobject, an older, lower-level API. Preferframe_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 newPagein.value.wait_for_load_state()waits until the new tab has loaded (theloadevent by default).- There is no “switch to” command: each tab is its own
Pageobject, and you simply call methods on the one you want.bring_to_front()only changes which tab is visible in headed mode. context.pageslists all open pages in the context; closing a page removes it.- The practice page has two “Login Page” links, so
.firstpicks 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", ...)orpage.on(...)before the action to read, accept (optionally with prompt text) or dismiss them. - Use
frame_locator()(orcontent_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()orcontext.expect_page(); each tab is a separatePageobject.
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