首页 / Playwright 入门教程 / 多页面与多标签处理

Playwright 入门教程

多页面与多标签处理

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

Playwright多标签context.pagesnewPagepage 事件页面管理

25. 多页面与多标签处理

本节目标:能主动开标签页、能接住被动开的标签页,并在多个页面之间自如切换操作。

上一章讲了三层模型。这一章聚焦最下面那层:一个 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.hiddenfalse

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 弹出窗口。