mcpField
W
web-testing-playwright-e2e

web-testing-playwright-e2e

github.com/agents-inc测试与质量2699 次安装2 天前更新
代码仓库

关于 web-testing-playwright-e2e

面向 Playwright 端到端测试的实践指南,适用于编写真实浏览器用户流程测试。内容涵盖稳健定位器、重试断言、Fixture、网络模拟和视觉检查。

主要功能

  1. 01语义化定位器与重试断言
  2. 02隔离并行测试的 Fixture
  3. 03网络拦截与响应模拟
  4. 04截图和 ARIA 快照检查
  5. 05时钟控制与页面对象

适用场景

  1. 01测试跨页面用户流程
  2. 02模拟 API 以稳定 UI 测试
  3. 03检查视觉或无障碍输出
  4. 04在并行测试间共享设置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-testing-playwright-e2e

安装后在 Claude Code 中输入/web-testing-playwright-e2e确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
advanced-features.md
api-mocking.md
core.md
fixtures.md
page-objects.md
visual-testing.md

详细介绍

Playwright E2E 测试模式

快速指南: Playwright 通过完整的用户流程驱动真实浏览器。定位器是惰性查询,每次使用时都会重新解析;expect(locator) 匹配器会持续重试,直到通过,因此正确的测试无需等待,也无需处理过期句柄。每项测试都有自己的浏览器上下文;共享设置通过 fixture 提供,而非使用模块级变量,因为测试会在并行工作进程中运行。

详细资源:


<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>

评论

支持 Markdown