关于 web-testing-playwright-e2e
面向 Playwright 端到端测试的实践指南,适用于编写真实浏览器用户流程测试。内容涵盖稳健定位器、重试断言、Fixture、网络模拟和视觉检查。
主要功能
- 01语义化定位器与重试断言
- 02隔离并行测试的 Fixture
- 03网络拦截与响应模拟
- 04截图和 ARIA 快照检查
- 05时钟控制与页面对象
适用场景
- 01测试跨页面用户流程
- 02模拟 API 以稳定 UI 测试
- 03检查视觉或无障碍输出
- 04在并行测试间共享设置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-testing-playwright-e2e安装后在 Claude Code 中输入/web-testing-playwright-e2e确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/advanced-features.mdapi-mocking.mdcore.mdfixtures.mdpage-objects.mdvisual-testing.md详细介绍
Playwright E2E 测试模式
快速指南: Playwright 通过完整的用户流程驱动真实浏览器。定位器是惰性查询,每次使用时都会重新解析;
expect(locator)匹配器会持续重试,直到通过,因此正确的测试无需等待,也无需处理过期句柄。每项测试都有自己的浏览器上下文;共享设置通过 fixture 提供,而非使用模块级变量,因为测试会在并行工作进程中运行。
详细资源:
- examples/core.md — 用户流程、页面对象、网络模拟、配置、身份验证 fixture
- examples/page-objects.md — 基础页面继承与层级结构
- examples/api-mocking.md — 拦截和修改真实响应
- examples/visual-testing.md — 截图对比、遮罩、状态
- examples/fixtures.md — fixture 组合、种子数据、存储状态
- examples/advanced-features.md — Clock API、ARIA 快照、 工作进程级 fixture、轮询断言、无障碍断言
- reference.md — 定位器和断言表、配置与 CLI 查询、版本 变更、故障排查
<critical_requirements>
编写 Playwright 代码前
优先使用 getByRole()。 它以用户和屏幕阅读器查找元素的方式选择元素,因此能适应标记变更,并顺带验证可访问名称。
使用 expect(locator) 匹配器进行断言。 它们会持续重试,直到条件成立或超时,这样就完全不需要等待——固定等待时间要么太短,要么太长。
在触发请求的导航之前注册路由。 路由附加到上下文,只会影响设置路由后发出的请求,因此在 page.goto() 之后调用 page.route(),永远无法拦截原本要拦截的请求。
通过 fixture 为每项测试提供独立状态。 Worker 会并行运行,模块级变量在同一个 Worker 内共享,在下一个 Worker 中则不存在,因此将设置逻辑放在 fixture 中,才能确保两种情况下行为一致。
</critical_requirements>
自动检测: Playwright、@playwright/test、playwright.config、page.goto、test.describe、 test.extend、expect(page)、getByRole、getByLabel、getByTestId、toBeVisible、toHaveURL、 toHaveScreenshot、toMatchAriaSnapshot、page.route、route.fulfill、page.clock、toPass、devices
适用范围:
- 通过真实浏览器跨页面和来源的用户旅程
- 定位器策略、面向 Web 的断言和软断言
- 为测试拦截、模拟和修改网络流量
- 截图对比和 ARIA 树断言
- Fixture、Worker 作用域、并行运行和测试配置
其他部分处理:
- 纯函数的单元级检查——浏览器对输入/输出断言没有帮助
- 挂载单个组件并检查其 props——粒度不同,应由组件测试的负责方处理
- 应用自身的无障碍问题修复——此技能会断言无障碍树,但不会决定该树应是什么样子
- 整个测试套件的截图基线管理和审查工作流
定位器是查询,不是句柄。page.getByRole("button") 在被使用前不会解析任何内容,而且每次使用时都会重新解析——因此无需防范过期元素,也没有理由提前捕获元素。断言也遵循同样的模式:expect(locator).toBeVisible() 会持续轮询,直到超时。
其他一切都由此而来:固定等待、CSS 选择器和共享变量,都舍弃了该模式本可免费提供的能力。
<decision_framework>
使用 Hook 还是 Fixture 进行设置:
- 仅供一个
describe块使用 → 使用beforeEach,让设置逻辑留在读取它的位置。 - 跨文件使用,或需要拆卸 → 使用 Fixture,它同时承载设置和拆卸逻辑,并支持组合。
- 整个运行过程只需执行一次 → 在配置中使用
globalSetup。beforeAll是每个 worker 执行一次,而不是每次运行执行一次。 - 开销大且可在同一个 Worker 内安全共享 → 使用 Worker 作用域的 Fixture。
模拟网络还是实际请求:
- 第三方 API → 模拟。速率限制、成本和服务中断都不是测试要验证的内容。
- 自有 API,测试错误路径 → 模拟;某些状态无法按需触发。
- 自有 API,测试集成是否正确 → 让请求通过,连接测试数据库,并为每个功能保留一项这样的测试。
- 自有 API,仅测试 UI 行为 → 模拟,以提高速度和确定性。
</decision_framework>
核心模式
模式 1:测试结构
使用 test.describe 分组,并用 beforeEach 处理该组中每项测试都会重复的导航。
test.describe("Login", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/login");
});
test("successful login lands on the dashboard", async ({ page }) => {
await page.getByLabel(/email/i).fill(VALID_EMAIL);
await page.getByRole("button", { name: /sign in/i }).click();
await expect(page).toHaveURL("/dashboard");
});
});
完整代码:examples/core.md
模式 2:页面对象模型
在构造函数中定义定位器,在类上定义领域方法。多个测试都会操作同一页面时值得采用;对于一次性测试,内联定位器更清晰。
export class LoginPage {
readonly emailInput: Locator;
readonly signInButton: Locator;
constructor(page: Page) {
this.emailInput = page.getByLabel(/email/i);
this.signInButton = page.getByRole("button", { name: /sign in/i });
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
// ...
}
}
完整代码:examples/core.md——继承示例见 examples/page-objects.md
模式 3:定位器策略
优先使用描述元素 是什么 的查询;只有在没有角色或标签时,才回退到测试 ID。
await page.getByRole("button", { name: /submit/i });
await page.getByLabel(/email address/i);
await page.getByTestId("user-avatar"); // no semantic role available
// Narrow within a list instead of writing a fragile selector
await page
.getByRole("listitem")
.filter({ hasText: "Product A" })
.getByRole("button", { name: /add to cart/i })
.click();
// Exclude and combine (v1.33+)
page.getByRole("listitem").filter({ hasNot: page.getByText("Out of stock") });
page.getByRole("button").and(page.getByTitle("Subscribe"));
优先级表和角色映射:reference.md
模式 4:以 Web 为先的断言
每个 expect(locator) 匹配器都会重试,包括否定匹配器。
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole("progressbar")).not.toBeVisible();
// Soft assertions collect every failure in one run instead of stopping at the first
await expect.soft(page.getByTestId("avatar")).toBeVisible();
await expect.soft(page.getByText("Premium")).toBeVisible();
完整断言表:reference.md
模式 5:网络模拟
page.route() 按 URL 模式拦截;route.fulfill() 返回响应,route.abort() 使请求失败。
await page.route("**/api/users", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ id: "user-123", name: "Jane Doe" }),
}),
);
await page.route("**/api/users", (route) => route.abort("failed")); // network failure
完整代码:examples/core.md — 在 examples/api-mocking.md 中修改真实响应
模式 6:视觉回归
toHaveScreenshot() 首次运行时会写入基准截图,之后进行比较。屏蔽会自行变化的内容。
await expect(page).toHaveScreenshot("dashboard.png", {
mask: [page.getByTestId("current-time")],
animations: "disabled",
});
完整代码:examples/visual-testing.md
模式 7:自定义 fixture
base.extend 可添加用于页面对象和设置的 fixture。auto fixture 无需在测试中命名即可运行。
export const test = base.extend<{ loginPage: LoginPage; authenticated: void }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticated: [
async ({ context }, use) => {
await context.addCookies([SESSION_COOKIE]);
await use();
await context.clearCookies();
},
{ auto: true },
],
});
完整代码:examples/core.md — examples/fixtures.md 中的组合与数据初始化
模式 8:时钟控制(v1.45+)
推进时间,而不是等待时间。每次时钟调用之前都要先调用 clock.install()。
await page.clock.install({ time: new Date("2024-02-02T08:00:00") });
await page.goto("/dashboard");
await page.clock.fastForward("25:00");
await expect(page.getByText(/session expires/i)).toBeVisible();
完整代码:examples/advanced-features.md
模式 9:ARIA 快照(v1.49+)
用一个易读的代码块断言整个无障碍树。
await expect(page.getByRole("navigation")).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "Products"
`);
完整代码:examples/advanced-features.md
<red_flags>
注意事项
会导致运行时故障:
- 在断言前调用
page.waitForTimeout()——等待时间太短会导致测试不稳定,太长则会拖慢测试;重试匹配器已经涵盖了等待。 - 在触发请求的导航之后才注册
page.route()——路由永远不会匹配;应在goto之前、beforeEach中或 fixture 中设置。 - 使用 CSS 或 ID 选择器(
.btn-primary、#submit-btn)——它们会随样式和标记变化;按角色或标签查询则不会。 - 在测试之间共享模块级变量——下一个 worker 中会缺失,且在同一个 worker 中也会产生竞争;应使用 fixture。
- 在
page.route()中使用包含?或[]的 Glob 模式——自 v1.52 起不再支持;请使用正则表达式。 - 使用
route.continue()覆盖Cookie标头——自 v1.52 起会被拒绝;请使用context.addCookies()。 - 使用
_react/_vue选择器引擎——自 v1.58 起已移除;请使用按角色查询或测试 ID。
容易出乎意料的行为:
beforeAll每个 worker 运行一次,而不是每次运行一次——一次性设置应放在globalSetup中。toBeVisible()会等待元素可见,而toBeAttached()只检查 DOM;隐藏元素也满足后者。- 自 v1.50 起,对不可编辑元素调用
toBeEditable()会抛出异常,而不是返回 false。 - 截图会因操作系统、浏览器构建版本和字体渲染而异——仅与在 CI 相同环境中生成的基准截图进行比较。
- 未屏蔽动态内容(时间戳、广告、头像)的截图,会因为内容而非布局发生变化导致失败。
- 断言继承的是配置中的
expect.timeout,而不是测试超时时间——慢页面会先导致断言失败。
</red_flags>