首页 / Playwright 入门教程 / 弹出窗口 popups 处理

Playwright 入门教程

弹出窗口 popups 处理

本教程共 59 篇 · 第 26 篇 · 更新于 2026-08-04 · 约 8 分钟阅读

Playwrightpopup弹出窗口waitForEventwindow.openOAuth

26. 弹出窗口 popups 处理

本节目标:能捕获并操作由页面打开的 popup 弹出窗口,分清它和普通新标签页在 API 上的区别。

上一章讲了多标签。这一章讲一种特殊的新页面——popup。

先澄清一个容易混的概念。这里的 popup(弹出窗口)不是 alert() 那种系统对话框(那是第 21 章的 Dialog(对话框)),也不是页面上用 div 做的模态框(那是普通元素)。

popup 指的是由当前页面打开的另一个浏览器窗口或标签页。触发方式有两种:点击 target="_blank" 的链接,或者调用 window.open()

上一章我们用 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 属于同一个 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 会同时触发 popuppage 两个事件,别重复处理。
  • 先挂等待、再触发动作,第一行不要 await
  • title() / url() 前先 waitForLoadState();用定位器和断言则不必。
  • popup 与主页面同属一个 context,共享 cookie 和配置。
  • target="_blank"window.open() 在 API 层面没区别。
  • 第三方登录能测但不该常测,优先复用登录态或 mock。

下一章换个方向:怎么让浏览器假装自己是一台手机。