Skip to content

Chapter 7 · Multi-Page, Popups, and iframes ​

Chapter goals: Master multi-tab management within a BrowserContext, learn to capture popups with expect_popup / context.on("page"), and confidently operate elements inside iframes using frame_locator.

7.1 Page and BrowserContext Relationship ​

A BrowserContext can contain multiple Pages — each Page corresponds to one tab or popup window. This model has two key properties:

  • Shared within context: all pages in the same context share Cookies, localStorage, network routes (context.route), and emulation config (viewport, locale, etc.);
  • Isolated between contexts: different contexts are completely isolated — this is the cornerstone of Playwright's test isolation.
python
# Basic multi-tab management
page_one = context.new_page()          # First tab
page_two = context.new_page()          # Second tab

all_pages = context.pages              # List of all pages in this context
print(len(all_pages))                  # -> 2

# Each page is an independent object; no need to "switch focus"
page_one.goto("https://example.com")
page_two.goto("https://playwright.dev")

No need for "bring to front"

Every Playwright Page behaves as if it is in the foreground — no need to switch window handles like in Selenium. This is one of the biggest UX differences between the two tools.

7.2 Capturing New Tabs: expect_page and expect_popup ​

Clicking a target="_blank" link or triggering window.open creates a new page. Core principle: register the wait first, then fire the action — because the new page is created during the action, searching for it after the fact leads to race conditions.

If the action produces a popup on the current page, use page.expect_popup(); for any new tab from any source, use context.expect_page():

python
from playwright.sync_api import sync_playwright, expect

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()
    page.goto("https://demo.playwright.dev/popup")

    # Method 1: Assertive wait for popup (recommended when triggered by this page)
    with page.expect_popup() as popup_info:
        page.get_by_text("open the popup").click()
    popup = popup_info.value              # The new Page object
    popup.wait_for_load_state()
    print(popup.url)
    popup.get_by_role("button").click()

    # Method 2: Capture any new tab in the context (when source is uncertain)
    with context.expect_page() as new_page_info:
        page.get_by_text("open new tab").click()
    another = new_page_info.value
    print(another.title())

    browser.close()

Both expect context managers return an EventContextManager; .value is the new Page. They must wrap the line that triggers the action; the default timeout is 30 seconds (overridable with timeout=).

7.3 Event-Style Listening: context.on("page") ​

When new pages may appear at any time (e.g., a countdown timer auto-opens a popup), event listening is more appropriate than expect:

python
opened_pages = []

def handle_page(page):
    """Record each newly opened page; grab title after it loads"""
    page.wait_for_load_state()
    opened_pages.append(page)

context.on("page", handle_page)   # Register once, valid for the context lifetime

# ... run test steps ...
assert len(opened_pages) == 1
print(opened_pages[0].title())

page.on("popup", handler) is the page-level variant — it only receives popups related to this page, with more precise semantics. Practical recommendation: prefer expect_popup/expect_page (they have timeout protection and clear failure messages); only fall back to event listening when you truly cannot predict the timing.

7.4 iframes: frame_locator and Frame Objects ​

iframes are a classic pain point in E2E testing: the main document's DOM simply does not contain elements inside the iframe, so normal locators wait until timeout. Playwright offers two paths:

Path 1: frame_locator (recommended). It returns a locator factory scoped to the frame, chainable into nested frames:

python
# Enter the iframe with class .frame-class, operate the input inside
username = page.frame_locator(".frame-class").get_by_label("User Name")
username.fill("John")

# Nested iframe: chain downward — outer is #my-frame, inner matches iframe[src*="inner"]
inner_btn = (
    page.frame_locator("#my-frame")
        .frame_locator('iframe[src*="inner"]')
        .get_by_role("button")
)
inner_btn.click()

# Assertions also live in the frame scope
expect(page.frame_locator("#status-frame").get_by_text("OK")).to_be_visible()

Path 2: Frame object. Suitable when you find the frame by name or URL and need to batch-operate:

python
# Find by iframe name attribute
frame = page.frame(name="frame-login")
# Find by URL regex
frame = page.frame(url=r".*domain.*login.*")

if frame:                       # Note: returns None, not an exception, when not found
    frame.fill("#username-input", "John")

Trade-off: frame_locator is a lazy locator with auto-retry and expect integration — use as the default; Frame objects are immediate references (may stale after iframe reload) — use for reading metadata like frame.url() or in non-locator APIs.

Shadow DOM is transparent to frame_locator: all locators pierce shadow roots by default (closed mode excluded).

7.5 Chapter Summary ​

  • A context holds multiple independent pages; no focus-switching needed;
  • expect_popup() for same-page popups, expect_page() for any new tab — register before acting;
  • context.on("page") for unpredictable timing; page.on("popup") for page-scoped events;
  • frame_locator is the preferred path for iframe interaction — lazy, retrying, composable;
  • Frame objects are immediate references for metadata and non-locator operations.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. To capture a popup opened by clicking a link on the current page, the recommended approach is?

2. Why does context.on("page") exist alongside expect_page?

3. How do you locate an element inside a nested iframe?

4. What happens when page.frame(name="missing") does not find a matching iframe?

🛠️ Hands-On Exercises ​

  1. Build a local HTML page with two iframes; use frame_locator chains to fill inputs in both and assert their content.
  2. Open a page with target="_blank" links; capture the popup with expect_popup and assert its title.
  3. Use context.on("page") to count how many tabs a site opens during navigation; compare with expect_page.

Proceed to Chapter 8: Network Interception and Mock.