弹出窗口 popups 处理
本教程共 59 篇 · 第 26 篇 · 更新于 2026-08-04 · 约 8 分钟阅读
26. 弹出窗口 popups 处理
本节目标:能捕获并操作由页面打开的 popup 弹出窗口,分清它和普通新标签页在 API 上的区别。
上一章讲了多标签。这一章讲一种特殊的新页面——popup。
先澄清一个容易混的概念。这里的 popup(弹出窗口)不是 alert() 那种系统对话框(那是第 21 章的 Dialog(对话框)),也不是页面上用 div 做的模态框(那是普通元素)。
popup 指的是由当前页面打开的另一个浏览器窗口或标签页。触发方式有两种:点击 target="_blank" 的链接,或者调用 window.open()。
popup 和 page 事件的关系
上一章我们用 context.on('page') 接住新标签。popup 也能用它接。
那为什么还要单独一个 popup 事件?
区别在作用域:
context.on('page'):这个上下文里任何新页面都会触发,包括你自己newPage()建的。page.on('popup'):只有由这个页面打开的弹窗才会触发。
一个 popup 出现时,两个事件都会触发。popup 事件是在 page 事件之外额外发出的。
用户点击 target="_blank" 链接
├──> context 触发 'page' 事件
└──> 源 page 触发 'popup' 事件
选哪个?我的原则很简单:
Tip知道是哪个页面打开的,就用
page.on('popup')。 它的语义更精确,代码读起来也更清楚——「这个页面弹出了什么」比「上下文里多了个页面」信息量大。只有在完全不确定来源时,才用
context.on('page')兜底。
标准用法
// 1. 先挂等待,不加 await
const popupPromise = page.waitForEvent('popup');
// 2. 触发动作
await page.getByText('打开帮助文档').click();
// 3. 拿到 popup
const popup = await popupPromise;
// 4. 等它加载完
await popup.waitForLoadState();
// 5. 当普通页面用
await expect(popup).toHaveTitle('帮助中心');
await popup.getByRole('button', { name: '我知道了' }).click();
popup 就是一个标准的 Page 对象。定位器、断言、导航、截图,全都能用。
Warning第一行不能加
await——这条提醒前面已经反复出现,因为它真的是最高频的错误。加了
await就变成:先阻塞等 popup,但触发 popup 的点击排在后面,永远执行不到。死锁。
为什么要 waitForLoadState
拿到 popup 的那一刻,它可能还是 about:blank——窗口对象已经创建,但目标页面还没开始加载。
这时直接断言标题会拿到空字符串:
const popup = await popupPromise;
console.log(await popup.title()); // 可能是 ''
加一句 waitForLoadState() 就稳了:
const popup = await popupPromise;
await popup.waitForLoadState();
console.log(await popup.title()); // 正确的标题
不过要注意:用定位器和自动重试断言时,一般不需要手动等。因为它们本身就会重试。
const popup = await popupPromise;
// 这行会自动等元素出现,不用先 waitForLoadState
await expect(popup.getByRole('heading')).toHaveText('帮助中心');
真正需要 waitForLoadState() 的是 title()、url() 这类一次性取值的方法。
长期监听所有 popup
不确定什么时候会弹,挂监听:
page.on('popup', async popup => {
await popup.waitForLoadState();
console.log('弹出了:', await popup.title());
});
一个实用场景是「确保测试过程中不会意外弹窗」:
import type { Page } from '@playwright/test'; // 只借类型,不影响运行
const popups: Page[] = [];
page.on('popup', p => popups.push(p));
// ... 一堆操作 ...
expect(popups, '不应该弹出任何窗口').toHaveLength(0);
两种触发方式的差异
target="_blank" 链接是最常见的形式:
<a href="/help" target="_blank">帮助文档</a>
浏览器一般把它开成新标签页。
window.open() 更灵活,可以指定窗口尺寸:
<button onclick="window.open('/help', 'helpWin', 'width=600,height=400')">
帮助
</button>
带尺寸参数时,浏览器通常开成独立小窗口。
对 Playwright 来说,两者没有区别——都触发 popup 事件,都返回 Page 对象。你不用关心它在真实浏览器里长什么样。
有一点要注意:popup 的视口尺寸可能和主页面不同。需要统一时手动设:
await popup.setViewportSize({ width: 1280, height: 720 });
popup 和主页面的关系
popup 属于同一个 context。这意味着:
- 共享 cookie 和 localStorage。
- 共享 context 级配置(语言、时区、权限)。
- 关闭 context 时一起关闭。
同时它有个 JS 层面的关联:popup 里的 window.opener 指向主页面。第三方登录就是靠这个把结果传回来的。
Note如果链接带了
rel="noopener",window.opener会是null。这是常见的安全实践。碰到「登录成功但主页面没反应」,可以先查这个属性。
典型场景:第三方登录
OAuth 登录几乎都是 popup 流程。完整写法:
import { test, expect } from '@playwright/test';
test('用第三方账号登录', async ({ page }) => {
await page.goto('https://example.com/login');
// 接住授权弹窗
const popupPromise = page.waitForEvent('popup');
await page.getByRole('button', { name: '使用 GitHub 登录' }).click();
const popup = await popupPromise;
// 在弹窗里完成授权
await expect(popup).toHaveURL(/github\.com\/login/);
await popup.getByLabel('Username').fill('testuser');
await popup.getByLabel('Password').fill('testpass');
await popup.getByRole('button', { name: 'Sign in' }).click();
await popup.getByRole('button', { name: 'Authorize' }).click();
// 弹窗一般会自己关掉
await popup.waitForEvent('close');
// 回到主页面看结果
await expect(page.getByText('testuser')).toBeVisible();
});
popup.waitForEvent('close') 用来等弹窗自行关闭。授权完成后关窗是标准流程,等它关掉再断言主页面比较稳。
Warning真实项目里别这么测第三方登录。你不控制 GitHub 的页面,它改个按钮文案你的测试就挂了,而且慢。
更好的做法是用
storageState复用登录态(第 33 章),或者直接 mock 掉授权接口(第 31 章)。这个例子只是演示 popup 的 API。
关闭 popup
弹窗不会自己消失,用完手动关:
await popup.close();
关掉之后主页面照常可用,不用做任何「切回去」的动作。
如果弹窗是自己关的,可以先判断:
if (!popup.isClosed()) {
await popup.close();
}
测试结束时 context 关闭,所有 popup 一起关,所以不关也不会泄漏。但测试中途开了很多 popup 的话,随手关掉更干净。
小结
- popup 指页面打开的新窗口或标签,不是 Dialog,也不是 div 模态框。
- 用
page.waitForEvent('popup')捕获,语义比context.waitForEvent('page')更精确。 - 一个 popup 会同时触发
popup和page两个事件,别重复处理。 - 先挂等待、再触发动作,第一行不要
await。 - 用
title()/url()前先waitForLoadState();用定位器和断言则不必。 - popup 与主页面同属一个 context,共享 cookie 和配置。
target="_blank"和window.open()在 API 层面没区别。- 第三方登录能测但不该常测,优先复用登录态或 mock。
下一章换个方向:怎么让浏览器假装自己是一台手机。