第 7 章 · 多页面、Popup 与 iframe
本章目标:掌握 BrowserContext 中多标签页的管理方式,学会用
expect_popup/context.on("page")捕获弹窗,并能用frame_locator从容操作 iframe 内的元素。
7.1 Page 与 BrowserContext 的关系
一个 BrowserContext 可以包含多个 Page——每个 Page 对应一个标签页或弹出窗口。这个模型有两个关键性质:
- 上下文内共享:同一 context 里的所有页面共享 Cookie、localStorage、网络路由(
context.route)和仿真配置(视口、locale 等); - 彼此独立:不同 context 之间完全隔离,这正是 Playwright 测试隔离性的基石。
# 多标签页的基本管理
page_one = context.new_page() # 新建第一个标签页
page_two = context.new_page() # 新建第二个标签页
all_pages = context.pages # 获取当前 context 的全部页面列表
print(len(all_pages)) # -> 2
# 每个页面都是独立对象,无需"切换焦点"即可直接操作
page_one.goto("https://example.com")
page_two.goto("https://playwright.dev")无需"bring to front"
Playwright 的每个 Page 都表现得像处于前台激活状态,不需要像 Selenium 那样先切换 window handle 再操作。这是两者使用体验上最大的差异之一。
7.2 捕获新标签页:expect_page 与 expect_popup
点击 target="_blank" 链接或触发 window.open 时会产生新页面。核心原则:先注册等待,再执行动作——因为新页面是在动作过程中产生的,动作结束后才去"找"它会有竞态。
如果动作产生的是本页面的 popup,用 page.expect_popup();如果是任意来源的新标签页,用 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") # 示例站点
# 方式一:断言式等待 popup(推荐用于确定由本页触发的场景)
with page.expect_popup() as popup_info:
page.get_by_text("open the popup").click()
popup = popup_info.value # 弹出的新 Page 对象
popup.wait_for_load_state()
print(popup.url) # popup 的地址
popup.get_by_role("button").click()
# 方式二:捕获 context 内任何新标签页(来源不确定时)
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()两个 expect 上下文管理器都返回 EventContextManager,.value 就是新 Page。注意它们必须包裹触发动作的那一行,超时默认 30 秒(可用 timeout= 参数覆盖)。
7.3 事件式监听:context.on("page")
当新页面可能在任意时刻出现(比如倒计时结束自动弹窗),事件监听比 expect 更合适:
opened_pages = []
def handle_page(page):
"""记录每个新打开的页面,等它完成加载后再取标题"""
page.wait_for_load_state()
opened_pages.append(page)
context.on("page", handle_page) # 注册一次,整个 context 生命周期有效
# ... 执行测试步骤 ...
assert len(opened_pages) == 1
print(opened_pages[0].title())page.on("popup", handler) 是它的页面级版本,只会收到与本页相关的 popup,语义更精确。实战建议:能定位触发动作就用 expect_popup/expect_page(有超时保护、失败信息清晰),只有真正无法预测时机时才用事件监听。
7.4 iframe:frame_locator 与 Frame 对象
iframe 是 E2E 测试的经典痛点:主文档的 DOM 里根本没有 iframe 内部的元素,普通 locator 会一直等到超时。Playwright 提供两条路径:
路径一:frame_locator(推荐)。它返回一个"作用域限定在 frame 内"的定位器工厂,可以链式深入嵌套 frame:
# 进入 class 为 .frame-class 的 iframe,操作其中的输入框
username = page.frame_locator(".frame-class").get_by_label("User Name")
username.fill("John")
# 嵌套 iframe:链式下钻,第一层是 #my-frame,内部又有 iframe[src*="inner"]
inner_btn = (
page.frame_locator("#my-frame")
.frame_locator('iframe[src*="inner"]')
.get_by_role("button")
)
inner_btn.click()
# 断言同样在 frame 作用域内进行
expect(page.frame_locator("#status-frame").get_by_text("OK")).to_be_visible()路径二:Frame 对象。适合按 name 或 URL 找到 frame 后批量操作的场景:
# 按 iframe 的 name 属性查找
frame = page.frame(name="frame-login")
# 按 URL 正则查找
frame = page.frame(url=r".*domain.*login.*")
if frame: # 注意:找不到时返回 None 而不是抛错!
frame.fill("#username-input", "John")两者的取舍:frame_locator 是惰性定位器,自动重试且能配合 expect 断言,应作为首选;Frame 对象是即时引用(iframe 重载后旧引用可能失效),适合读取 frame.url() 等元信息或在非 locator API 中使用。
7.5 页面关闭时序与常见坑
page.close()只关闭该标签页;context.close()关闭全部页面并回收资源,pytest-playwright 的 fixture 默认在每个测试结束时做后者;- popup 的加载时序:
expect_popup().value拿到的 Page 可能还没加载完,操作前先wait_for_load_state()或依赖 locator 的自动等待; - 在 handler 里调用阻塞方法要小心:同步 API 中事件回调与主逻辑在同一线程,回调里做耗时操作会拖慢整个流程;
- 判断某个页面是否已关闭可用
page.is_closed()。
本章小结
- 一个 context 多个 page:共享存储与路由,无需手动切换焦点;
- 已知触发动作 →
page.expect_popup()/context.expect_page()包裹动作;未知时机 →context.on("page")事件; - iframe 优先用
frame_locator(惰性 + 可断言),page.frame()返回即时引用、找不到时为None; - popup 拿到后注意加载时序;资源清理交给
context.close()。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 点击 target="_blank" 链接后获取新标签页,最推荐的写法是?
2. 关于 page.expect_popup() 和 context.expect_page() 的区别,正确的是?
3. 操作 iframe 内元素的首选方式是?
4. page.frame(name="frame-login") 在目标 frame 不存在时会怎样?
🛠️ 动手实践
- 写一个脚本访问
https://demo.playwright.dev/popup,用expect_popup捕获弹窗并断言其标题。 - 构造一个本地 HTML 文件(内含两层嵌套 iframe),用链式
frame_locator操作最内层按钮并断言文本变化。 - 用
context.on("page")统计一段交互中总共打开了几个标签页,并在结束时只保留主页、逐一关闭其余页面。