Playwright 浏览器上下文隔离机制:Browser Context 原理、用法与源码解析
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();
为什么测试隔离很重要
官方文档总结了隔离带来的三点核心价值:
- 失败不扩散(No failure carry-over):一个测试失败不会影响其他测试,不会出现“连锁失败”(cascading failures);
- 易于调试:错误与偶发失败(flakiness)可以单独复现——你可以只跑某一个测试,跑任意多次;
- 无需关心执行顺序:并行、分片(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 → _contextFactory → browser.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 }],
从这段实现可以确认几个关键事实:
- 测试级作用域:
_contextFactory的 fixture 声明了{ scope: 'test' },即每个测试都会重新执行该工厂,创建并回收自己的上下文,这就是“每测试一个上下文”的机制来源; - 默认页从上下文派生:
pagefixture 依赖context,通过await context.newPage()生成默认页面; - 自动关闭:测试结束后统一
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' |
storageState、proxy、timezoneId、userAgent、geolocation 等 |
未设置(可选) |
这些选项均可在配置文件中通过 use 或测试内的 test.use() 修改,最终作为 BrowserContextOptions 传给 browser.newContext()。
隔离性的实证:仓库测试用例
仓库自带测试直接验证了上下文之间的状态隔离,可作为“隔离是否真的成立”的可执行证据:
- tests/library/browsercontext-basic.spec.ts 中的
should isolate localStorage and cookies @smoke用例:创建两个上下文context1/context2,各自写入同名localStorage与 cookie,随后断言两边互相看不到对方的值(page1只读到自己写入的page1,page2只读page2),并断言上下文之间页面对象互不共享; - tests/library/browsercontext-add-cookies.spec.ts 包含
should isolate cookies in browser contexts、should isolate session cookies、should 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 的实现,还有几点约束值得注意:
context/pagefixture 不能用于beforeAll/afterAll:源码中工厂函数会检查当前所处 hook 类型,若在beforeAll/afterAll中访问会抛出错误,提示上下文是按测试粒度创建的。官方给出的替代方案是:需要跨测试复用页面时用browser.newContext()手动创建;需要“每个测试前配置页面”则应写在beforeEach中;- 上下文复用是显式开关:默认
reuseContext为false(源码中 fixture 默认值可查),只有配合contextReuse/PW_TEST_REUSE_CONTEXT且未开启录像时才可能复用上下文,隔离性默认不被破坏; - 录制视频时注意:
_contextFactory会把recordVideo选项合并进newContext()参数,即每个测试的默认上下文都可能是带视频录制的上下文,录像保存时机与测试状态(通过/失败/超时)绑定。
小结
Playwright 的隔离模型可以概括为一句话:上下文是廉价的、干净起步的隔离单元,测试运行器为每个测试自动创建一个并在测试结束时销毁。默认路径下无需关心状态清理;需要模拟多用户时改用 browser fixture 手动创建多个上下文;需要设备、权限、语言区域等差异时,则通过上下文的选项(见 模拟指南)表达。理解 packages/playwright/src/index.ts 中 context / page / _contextFactory 三个 fixture 的协作关系,能帮助你解释并行执行下的行为差异、定位“默认页面为何是 1280×720”这类细节,以及正确设计需要跨测试共享状态的特殊场景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00