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 usingframe_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.
# 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():
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:
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:
# 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:
# 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_locatoris the preferred path for iframe interaction — lazy, retrying, composable;Frameobjects 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
- Build a local HTML page with two iframes; use
frame_locatorchains to fill inputs in both and assert their content. - Open a page with
target="_blank"links; capture the popup withexpect_popupand assert its title. - Use
context.on("page")to count how many tabs a site opens during navigation; compare withexpect_page.
Proceed to Chapter 8: Network Interception and Mock.