多页面与多标签处理
本教程共 59 篇 · 第 25 篇 · 更新于 2026-08-04 · 约 9 分钟阅读
本节目标:能主动开标签页、能接住被动开的标签页,并在多个页面之间自如切换操作。
上一章讲了三层模型。这一章聚焦最下面那层:一个 BrowserContext(浏览器上下文,下文简称 context)里有多个 Page(页面)时怎么办。
先说个让很多人意外的事实:Playwright 里不需要「切换标签页」。
每个页面都是激活态
用过 Selenium 的人会习惯先切窗口再操作:拿句柄、switchTo、干活、切回来。忘了切就报错。
Playwright 没这套东西。它的每个 Page 对象都独立持有一条通信通道,直接对着那个页面下指令。
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto('https://example.com/a');
await page2.goto('https://example.com/b');
// 交替操作,不用切换,也不用管哪个在前台
await page1.getByRole('button', { name: '保存' }).click();
await page2.getByRole('button', { name: '刷新' }).click();
await page1.getByText('已保存').waitFor();
从每个页面自己的角度看,它都是「被聚焦的活动页面」。定时器照常跑,动画照常播,document.hidden 是 false。
Note有个
page.bringToFront()方法,作用是把页面在真实浏览器窗口里提到最前。它不影响操作能力,只在你需要真实的视觉前后关系时才用(比如某些截图场景)。日常测试基本用不上。
主动创建标签页
const page = await context.newPage();
await page.goto('https://example.com');
用测试运行器时,context 也是能直接注入的 Fixture(夹具):
import { test } from '@playwright/test';
test('同时看两个页面', async ({ context, page }) => {
// page 是默认页,再开一个
const secondPage = await context.newPage();
await secondPage.goto('https://example.com/settings');
await page.goto('https://example.com/profile');
// 两个页面都能操作
});
获取全部页面:context.pages()
const allPages = context.pages();
console.log(`当前有 ${allPages.length} 个页面`);
for (const p of allPages) {
console.log(await p.title());
}
pages() 是同步方法,返回当前时刻的页面数组。已关闭的页面不在里面。
它常用来做兜底处理,比如「不管开了几个,全部关掉」:
for (const p of context.pages()) {
await p.close();
}
Warning
pages()是快照,不是响应式的。刚点完链接就调它,新页面可能还没建好,数组里就没有。要拿刚开的页面,用下面的事件方式。
接住被动打开的标签页
页面上有 <a target="_blank">,点了会开新标签。这个标签不是你建的,得接。
标准写法(第 20 章那个模式又出现了):
// 1. 先挂等待,不要 await
const pagePromise = context.waitForEvent('page');
// 2. 触发动作
await page.getByText('在新标签中打开').click();
// 3. 拿到新页面
const newPage = await pagePromise;
// 4. 等它加载完再操作
await newPage.waitForLoadState();
await expect(newPage).toHaveTitle('详情页');
await newPage.getByRole('button', { name: '确认' }).click();
Warning第一行绝对不能加
await。加了就死锁:等待事件的代码在阻塞,触发事件的点击永远执行不到。顺序永远是:挂等待 → 触发动作 → await 结果。
新页面拿到手之后,用法和普通 page 完全一样。定位、点击、断言,一个不缺。
不知道什么时候会开:长期监听
有些页面会在异步逻辑里自己 window.open(),你抓不到确切的触发点。这时挂长期监听:
context.on('page', async newPage => {
await newPage.waitForLoadState();
console.log('新开了页面:', await newPage.title());
});
这种写法的代价和第 20 章讲下载时一样:控制流被分叉,主流程可能先跑完。所以有明确触发点时,优先用 waitForEvent。
多标签共享什么
同一个 context 下的标签页,共享这些:
- Cookie
- localStorage / sessionStorage(同源)
- HTTP 缓存
- 权限授予
- 视口大小、语言、时区等 context 级配置
所以「在 A 标签登录,在 B 标签验证已登录」这种测试,一个 context 就能搞定:
test('登录态在标签间共享', async ({ context }) => {
const loginPage = await context.newPage();
await loginPage.goto('https://example.com/login');
await loginPage.getByLabel('用户名').fill('mashangxue');
await loginPage.getByLabel('密码').fill('secret');
await loginPage.getByRole('button', { name: '登录' }).click();
await loginPage.waitForURL('**/dashboard');
// 新标签直接就是登录态
const anotherPage = await context.newPage();
await anotherPage.goto('https://example.com/profile');
await expect(anotherPage.getByText('mashangxue')).toBeVisible();
});
反过来,如果你要验证「两个不同用户」,一个 context 就不够了,必须开两个(见上一章)。
关闭页面
await page.close();
关闭后再操作会抛错。想提前判断:
if (!page.isClosed()) {
await page.close();
}
也可以监听关闭事件:
page.on('close', () => console.log('页面关闭了'));
用测试运行器时,测试结束会自动关掉整个 context,里面的页面一起没。手动开的标签页不用特意关——除非你想在测试中途释放资源。
带未保存提示的页面要跑 unload 处理器,得显式打开:
await page.close({ runBeforeUnload: true });
这会触发 beforeunload 对话框,处理方式见第 21 章。
常见场景:并行操作两个页面
多标签的一个实用价值是并行等待。比如两个页面都要加载慢接口:
const page1 = await context.newPage();
const page2 = await context.newPage();
// 同时导航,总耗时约等于慢的那个
await Promise.all([
page1.goto('https://example.com/report-a'),
page2.goto('https://example.com/report-b'),
]);
await expect(page1.getByRole('table')).toBeVisible();
await expect(page2.getByRole('table')).toBeVisible();
Promise.all 在这里是安全的,因为两个页面互不干扰。
Tip别对同一个页面的多个操作用
Promise.all。同一页面上的动作有先后依赖,并发下去结果不可预期。跨页面才适合并行。
一个完整例子
import { test, expect } from '@playwright/test';
test('从列表页新标签打开详情', async ({ context, page }) => {
await page.goto('https://example.com/articles');
// 接住新标签
const detailPromise = context.waitForEvent('page');
await page.getByRole('link', { name: '深入理解定位器' }).click();
const detailPage = await detailPromise;
await detailPage.waitForLoadState();
// 新标签上断言
await expect(detailPage).toHaveURL(/\/articles\/\d+/);
await expect(detailPage.getByRole('heading')).toHaveText('深入理解定位器');
// 原页面还在,照样能操作
await expect(page.getByRole('link', { name: '深入理解定位器' })).toBeVisible();
// 确认现在有两个页面
expect(context.pages()).toHaveLength(2);
await detailPage.close();
});
小结
- Playwright 不需要切换标签页,每个 Page 对象都能直接操作,
bringToFront()只影响视觉层。 - 主动开标签用
context.newPage()。 - 拿全部页面用
context.pages(),注意它是快照,新页面可能还没进来。 - 接住被动开的标签用
context.waitForEvent('page'),先挂等待再触发动作。 - 不确定触发点时用
context.on('page')长期监听,但控制流会分叉。 - 同 context 的标签共享 cookie 和存储,可用来测登录态传递。
- 跨页面操作可以
Promise.all并行,同页面不要。
下一章讲一种特殊的新页面:popup 弹出窗口。