首页
/ Playwright 浏览器上下文隔离机制:Browser Context 原理、用法与源码解析

Playwright 浏览器上下文隔离机制:Browser Context 原理、用法与源码解析

2026-09-06 12:49:40作者:郁楠烈Hubert

Playwright 的测试默认运行在“干净起步”的隔离环境中——每个测试独享一个 BrowserContext(类似无痕浏览器配置文件)。本文基于仓库官方文档 docs/src/browser-contexts.md 展开,结合测试运行器的 fixture 源码与隔离性测试用例,讲解测试隔离为何重要、Playwright 如何自动创建与销毁上下文、如何在单个测试内手动创建多个上下文模拟多用户场景,并深入 _contextFactory 的实现细节。读完后你将掌握 Playwright 隔离模型的设计意图,并能在库模式(library mode)下正确管理上下文的生命周期。

什么是测试隔离(Test Isolation)

测试隔离是指每个测试与其他测试完全隔离:每个测试独立运行,拥有自己独立的 localStorage、sessionStorage、cookies 等浏览器状态。Playwright 通过 BrowserContext 实现这一点——上下文等价于“无痕浏览器配置文件”(incognito-like profile),具有以下特点:

  • 创建速度快、成本低:上下文不需要启动新浏览器进程,即使多个上下文共享同一个浏览器实例,也完全互相隔离;
  • 天然干净:每个上下文从零开始,没有任何历史状态。

在使用 Playwright 作为测试运行器(@playwright/test)时,框架会为每个测试创建一个上下文,并在其中提供一个默认的 page 页面。测试中通过 fixture 直接注入这两个对象:

import { test } from '@playwright/test';

test('example test', async ({ page, context }) => {
  // "context" 是一个为本测试专门创建的、完全隔离的 BrowserContext。
  // "page" 就属于这个 context。
});

test('another test', async ({ page, context }) => {
  // 第二个测试中的 "context" 和 "page" 与第一个测试
  // 完全隔离,互不影响。
});

如果不使用测试运行器,而是以库的方式调用 Playwright,则可以手动创建上下文:

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

其他语言的等价写法:

// Java
Browser browser = chromium.launch();
BrowserContext context = browser.newContext();
Page page = context.newPage();
# Python(异步与同步 API 相同,仅 await 的差异)
browser = playwright.chromium.launch()
context = browser.new_context()
page = context.new_page()
// C#
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.LaunchAsync();
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();

为什么测试隔离很重要

官方文档总结了隔离带来的三点核心价值:

  1. 失败不扩散(No failure carry-over):一个测试失败不会影响其他测试,不会出现“连锁失败”(cascading failures);
  2. 易于调试:错误与偶发失败(flakiness)可以单独复现——你可以只跑某一个测试,跑任意多次;
  3. 无需关心执行顺序:并行、分片(sharding)等执行策略下,不必考虑测试间的顺序依赖。

两种隔离策略:从零开始 vs 中途清理

做测试隔离有两类策略:

  • 每个测试前清理(cleanup in between):问题在于清理工作很容易遗漏,而且有些状态根本无法清理,例如“已访问过的链接”(visited links)这类浏览历史状态;
  • 从零开始(start from scratch):每个测试都拿到全新环境。这正是 Playwright 的路线——如果测试失败,你只需要在这一个测试内部寻找原因,调试范围被精确限定。

Playwright 用浏览器上下文落地这一策略:运行测试时,每次执行都会创建一个全新的 browser context。使用测试运行器时上下文默认自动创建;库模式下则需手动创建(见上一节代码)。

此外,浏览器上下文还可用于模拟多页面场景,包括移动设备、权限(permissions)、语言区域(locale)和颜色模式(color scheme)等,详见仓库中的 模拟(Emulation)指南

源码视角:上下文如何按测试创建与销毁

从源码结构看,测试运行器中“每测试一上下文”的行为由 packages/playwright/src/index.ts 中的 fixture 定义实现,关键链路是 context_contextFactorybrowser.newContext(...)

// packages/playwright/src/index.ts(节选)
_contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions }, use, testInfo) => {
  const contexts = new Map<BrowserContext, { close: () => Promise<void>, pagesWithVideo: Page[] }>();
  // ...
  await use(async options => {
    const context = await browser.newContext({ ...videoOptions, ...options }) as BrowserContextImpl;
    // 测试结束后调用 close() 关闭上下文
    const close = async () => {
      // ...
      const closeReason = testInfo.status === 'timedOut'
        ? 'Test timeout of ' + testInfo.timeout + 'ms exceeded.'
        : 'Test ended.';
      await context.close({ reason: closeReason });
      // ...
    };
    return { context, close };
  });
  await Promise.all([...contexts.values()].map(data => data.close()));
}, { scope: 'test', title: 'context', box: true }],

从这段实现可以确认几个关键事实:

  1. 测试级作用域_contextFactory 的 fixture 声明了 { scope: 'test' },即每个测试都会重新执行该工厂,创建并回收自己的上下文,这就是“每测试一个上下文”的机制来源;
  2. 默认页从上下文派生page fixture 依赖 context,通过 await context.newPage() 生成默认页面;
  3. 自动关闭:测试结束后统一 Promise.all 关闭本次测试创建过的所有上下文,关闭原因会区分“测试超时”与“测试正常结束”,便于在浏览器端定位问题。

同一文件还定义了上下文的默认选项与默认值(来自 use(test.use) 的 option 映射),例如:

选项 默认值
viewport { width: 1280, height: 720 }(可设为 null 禁用)
locale 'en-US'
colorScheme 'light'
hasTouch false
acceptDownloads true(测试运行器中默认开启)
bypassCSP false
offline false
serviceWorkers 'allow'
reducedMotion / contrast / forcedColors 均为 'no-preference' / 'no-preference' / 'none'
storageStateproxytimezoneIduserAgentgeolocation 未设置(可选)

这些选项均可在配置文件中通过 use 或测试内的 test.use() 修改,最终作为 BrowserContextOptions 传给 browser.newContext()

隔离性的实证:仓库测试用例

仓库自带测试直接验证了上下文之间的状态隔离,可作为“隔离是否真的成立”的可执行证据:

  • tests/library/browsercontext-basic.spec.ts 中的 should isolate localStorage and cookies @smoke 用例:创建两个上下文 context1/context2,各自写入同名 localStorage 与 cookie,随后断言两边互相看不到对方的值(page1 只读到自己写入的 page1page2 只读 page2),并断言上下文之间页面对象互不共享;
  • tests/library/browsercontext-add-cookies.spec.ts 包含 should isolate cookies in browser contextsshould isolate session cookiesshould isolate persistent cookies 等一系列用例,覆盖会话 Cookie 与持久化 Cookie 的隔离,甚至验证了跨浏览器启动(两次 launch)之间 Cookie 也不会串扰。

单测试内创建多个上下文:模拟多用户

Playwright 允许在单个测试场景中创建多个浏览器上下文,适合测试聊天、协作编辑等多用户(multi-user)功能。此时需改用 browser fixture 手动创建两个隔离上下文,并各自打开页面独立交互:

import { test } from '@playwright/test';

test('admin and user', async ({ browser }) => {
  // 创建两个互相隔离的浏览器上下文
  const adminContext = await browser.newContext();
  const userContext = await browser.newContext();

  // 分别创建页面,两个上下文可独立操作
  const adminPage = await adminContext.newPage();
  const userPage = await userContext.newPage();
});

库模式下的多上下文示例:

const { chromium } = require('playwright');

// 创建 Chromium 浏览器实例
const browser = await chromium.launch();

// 创建两个互相隔离的浏览器上下文
const userContext = await browser.newContext();
const adminContext = await browser.newContext();

// 分别创建页面并独立操作
const adminPage = await adminContext.newPage();
const userPage = await userContext.newPage();
// Java
try (Playwright playwright = Playwright.create()) {
  Browser browser = playwright.chromium().launch();
  BrowserContext userContext = browser.newContext();
  BrowserContext adminContext = browser.newContext();
  // 分别创建页面并独立操作
}
# Python(同步 API)
with sync_playwright() as playwright:
    browser = playwright.chromium.launch()
    user_context = browser.new_context()
    admin_context = browser.new_context()
    # 分别创建页面并独立操作
// C#
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
await using var userContext = await browser.NewContextAsync();
await using var adminContext = await browser.NewContextAsync();

使用上下文 fixture 的注意事项

结合 packages/playwright/src/index.ts_contextFactory 的实现,还有几点约束值得注意:

  1. context / page fixture 不能用于 beforeAll / afterAll:源码中工厂函数会检查当前所处 hook 类型,若在 beforeAll/afterAll 中访问会抛出错误,提示上下文是按测试粒度创建的。官方给出的替代方案是:需要跨测试复用页面时用 browser.newContext() 手动创建;需要“每个测试前配置页面”则应写在 beforeEach 中;
  2. 上下文复用是显式开关:默认 reuseContextfalse(源码中 fixture 默认值可查),只有配合 contextReuse / PW_TEST_REUSE_CONTEXT 且未开启录像时才可能复用上下文,隔离性默认不被破坏;
  3. 录制视频时注意_contextFactory 会把 recordVideo 选项合并进 newContext() 参数,即每个测试的默认上下文都可能是带视频录制的上下文,录像保存时机与测试状态(通过/失败/超时)绑定。

小结

Playwright 的隔离模型可以概括为一句话:上下文是廉价的、干净起步的隔离单元,测试运行器为每个测试自动创建一个并在测试结束时销毁。默认路径下无需关心状态清理;需要模拟多用户时改用 browser fixture 手动创建多个上下文;需要设备、权限、语言区域等差异时,则通过上下文的选项(见 模拟指南)表达。理解 packages/playwright/src/index.tscontext / page / _contextFactory 三个 fixture 的协作关系,能帮助你解释并行执行下的行为差异、定位“默认页面为何是 1280×720”这类细节,以及正确设计需要跨测试共享状态的特殊场景。

登录后查看全文
热门项目推荐
相关项目推荐