浏览器自动化测试数据与状态卫生
为合成测试数据、浏览器状态和并行 worker 的清理流程明确所有权。
可靠的测试应在运行前写明输入、所有者、可观察结果和清理边界。新的 context 不能让共享账号、服务端记录或下载目录自动变得独立。请通过文档化的测试接口创建合成数据,不要使用真实客户数据、个人文件或凭据。可参考fixture 与隔离指南。
测试数据与状态所有权矩阵
| 资源 | 创建者 | 可变所有者 | 可观察结果 | 清理边界 |
|---|---|---|---|---|
| 合成账号 | 测试服务 | 场景 worker | 账号标题或访问结果 | 服务重置 API |
| 浏览器 context 与 cookies | fixture | fixture | 干净 context | 在 finally 中关闭 |
| 状态基线 | 测试仓库 | fixture 副本 | 状态加载路由 | 保留基线,删除副本 |
| 工件目录 | worker | worker | 简短回执 | worker 删除本地路径 |
| 远端任务 | 应用 | 服务 | 文档化状态 | 服务取消或过期 |
关闭 context 只证明浏览器管理的状态释放,不能证明服务端记录已删除。基线应只读;写出的副本要带 worker 和尝试编号。并行 worker 使用不同合成记录,或只串行化明确的共享变更。
失败 fixture
用缺少就绪标记的合成页面验证有界失败和清理:
import os from 'node:os';
import path from 'node:path';
import { mkdtemp, rm } from 'node:fs/promises';
import { chromium } from 'playwright';
import { expect } from '@playwright/test';
const dir = await mkdtemp(path.join(os.tmpdir(), 'state-hygiene-'));
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ baseURL: 'http://fixture.test' });
const page = await context.newPage();
await page.route('/fixtures/without-ready-marker', route =>
route.fulfill({ status: 200, contentType: 'text/html', body: '<main><p>Waiting</p></main>' })
);
let failure;
const remember = (label, error) => {
const current = new Error(`${label}: ${error.message}`, { cause: error });
failure = failure ? new AggregateError([failure, current], `${failure.message}; ${label} failed`) : current;
};
try {
await page.goto('/fixtures/without-ready-marker');
await expect(page.getByRole('status')).toHaveText('Ready', { timeout: 250 });
} catch (error) {
remember('fixture 断言', error);
} finally {
try {
await context.close();
} catch (error) {
remember('关闭 context', error);
}
try {
await rm(dir, { recursive: true, force: true });
} catch (error) {
remember('清理工件', error);
}
try {
await browser.close();
} catch (error) {
remember('关闭浏览器', error);
}
}
if (failure) throw failure;
决策表
| 观察 | 分类 | 下一步 | 未证明 |
|---|---|---|---|
| 找不到基线 | fixture | 失败并修复输入 | 服务可用性 |
| 出现旧 cookie | 浏览器状态 | 丢弃 context | 服务端会话失效 |
| worker 覆盖记录 | 测试数据 | 分配记录或串行化 | 浏览器隔离失败 |
| 断言成功但清理失败 | 基础设施 | 保留两种错误 | 远端删除 |
| 重试成功 | 结果不确定 | 查询支持的状态 | 首次尝试无害 |
检查状态转换
把状态看成基线、可见检查和清理组成的序列。切换账号必须走应用规定的退出流程并创建新 context;清理本地存储不等于让远端会话失效。状态快照应记录版本,损坏的副本应失败而不是静默重建。
保护密钥与工件
状态快照可能含有 cookie。限制文件权限,只保留场景、worker、结果和清理回执,不记录 token 或页面全文。worker 临时目录和 CI 留存由不同所有者管理。文件传输边界可参考上传下载测试指南。
检查重试与并行
文件名使用场景、worker 和尝试编号,不使用个人标识。超时后先查询支持的状态,再重复可能已到达服务端的变更。浏览器或应用升级后运行干净 context、过期状态、账号切换、并行输出和强制清理。
可重复性来自受控输入,而不是长期共享的浏览器。
固定实际需要的路由、fixture 模式、合成记录形状和浏览器配置。
这样变化才可归因于应用、fixture 或 runner。
不要使用会创建账号、授予权限并留下页面的全局准备步骤。
这种准备会把首个失败的原因与早先测试的影响混在一起。
启动 fixture 应说明已加载已知状态或完成文档化登录。
它不应输出用于登录的会话材料。
数据 fixture 应说明场景标签和重置结果。
它不应枚举记录来寻找碰巧可用的一条。
缺少预期输入就是 fixture 失败。
未计划的替代输入会让结果无法复现。
有些状态位于浏览器外,例如邮件模拟器或通知服务。
每个外部依赖都需要自己的合成命名空间和回执。
浏览器可以观察确认页,但不能声称所有后续投递已经发生。
属于服务的事实应通过其文档化测试接口确认。
关闭页面会释放该页面的 listeners 和 handles。
关闭 context 会释放该 context 管理的状态。
这些操作不会自行删除已复制文件、已上传对象或另一设备持有的 token。
在页面仍可用时执行文档化的退出流程。
随后关闭浏览器资源,再移除 worker 本地目录。
应用清理失败可以与浏览器关闭失败无关。
在简短回执中保留这两个观察结果。
不要搜索名称相似的 profile、目录或 context 来扩大清理范围。
删除失败时,恢复流程只能隔离自己已知的目录。
合成记录应可识别,但不应伪装成个人数据。
使用场景前缀、运行标签和专为断言设计的值。
测试边界时只改变一个已声明的输入属性。
重置应恢复已知 baseline,而不是猜测如何修复残留记录。
清理不完整的标签不得在下一次尝试中自动复用。
断言应描述用户或授权测试可在浏览器边界看到的内容。
本地存储项为空不能证明远端会话已经失效。
完成导航不能证明异步任务已经执行。
记录缺失信号、意外可见状态或文档化错误类别。
调整超时前,先检查唯一记录、唯一 context 和私有工件目录。
日常复查应先将失败回执与声明的契约对照,再调整等待时间。
BotBrowser 能力与限制
BotBrowser 可为授权的合成流程提供独立的 BrowserContexts,分别保存 cookies、存储和会话状态,用于验证新 context 不继承客户端状态。详见 BotBrowser 多账号隔离文档。BotBrowser 不替代 Playwright 或 Selenium 生命周期管理、应用清理、服务端会话失效和密钥管理,也不能保证过期 cookie 被接受、远端任务被取消或供应商记录被删除。
只记录场景、worker、可见结果和清理状态。合并 fixture 修改前运行干净 context、过期状态和本失败案例。