关于 playwright-e2e-testing
playwright-e2e-testing 是一套 Playwright 浏览器、端到端及 API 测试指导 Skill,面向编写和排查测试的开发者。它涵盖 SPA 导航、请求模拟、异步等待和用户可见回归证据等实践。
主要功能
- 01选择合适的测试边界
- 02组织测试目录与共享夹具
- 03等待异步 UI 状态
- 04使用面向用户的定位器
- 05模拟 API 请求与响应
- 06验证用户可见回归行为
适用场景
- 01编写浏览器和端到端流程测试
- 02测试 SPA 路由与重定向
- 03直接测试 API 端点
- 04排查 Playwright 测试失败
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ perdolique/ workflow --skill playwright-e2e-testing安装后在 Claude Code 中输入/playwright-e2e-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonevals/files/performance.mdverification.mdevals/files/harness/context.mdglobal.fixtures.tsevals/files/regressions/icon.htmlicon.spec.tspolling.htmlpolling.spec.tsresponsive.htmlresponsive.spec.tssearch.htmlsearch.spec.tswidget.htmlwidget.spec.tsreferences/configuration.mdfixtures.mdperformance.md详细介绍
使用 Playwright 进行浏览器、E2E 和 API 测试
本技能提供 Playwright 浏览器、E2E 和直接 API 测试的模式与约定,包括针对 SPA 的路由、API 模拟和异步导航技巧。
何时使用此技能
- 为用户流程编写浏览器或 E2E 测试
- 测试页面路由和重定向
- 测试对话框/模态框交互
- 通过 Playwright 的
APIRequestContext直接测试端点 - 为 Playwright 测试创建模拟、fixture 和断言
- 调试失败的 Playwright 浏览器、E2E 或 API 测试
- 根据任务和项目已采用的测试工具选择测试技能。根据相应上下文处理对模拟或测试数据的通用请求。
项目配置
设置或修改 playwright.config.ts 时,请参阅 references/configuration.md,了解 Web 服务设置、报告器、跟踪、屏幕截图和 CI 行为方面的约定。
选择测试边界
- 明确说明要测试的浏览器行为,以及测试应捕获的可能故障。先检查现有覆盖范围。纯计算应放在项目的单元测试中,直接 HTTP 契约应放在 API 测试中;浏览器测试则用于渲染、交互、请求连接、导航和浏览器状态。
- 如果在现有流程中添加断言即可保护同一场景,且无需再进行一次完整的设置,就应添加断言。若要保护的是不同故障,或需要不同状态,则应使用单独的用例。模拟不要覆盖测试声称要验证的决策。
测试导入
检查项目的测试目录,查找扩展了 Playwright test 对象的自定义 fixture 文件,通常用于 API 拦截或共享设置。如果存在,请从该文件导入 test 和 expect,以便测试使用项目的设置:
import { test, expect } from '../fixtures/global.fixtures.ts';
如果项目没有自定义 fixture,请从 Playwright 导入:
import { test, expect } from '@playwright/test';
始终可以直接导入独立类型:
import type { Page, Locator } from '@playwright/test';
配置共享浏览器 fixture,使其在应用出现 pageerror、console.error、水合警告和未解析组件时失败。复用项目的 fixture 时,保留这些检查。仅排除测试会显式断言的浏览器生成资源错误。
每项测试都使用提供的 page 和 context,以确保浏览器状态全新。只有在测试之间不会互相更改状态时,才共享不可变数据或设置资源。页面或上下文销毁前,应完成异步事件处理程序,即使测试失败也不例外。对于自有的控制台监听器,在 fixture 清理过程中调用 await page.removeAllListeners('console', { behavior: 'wait' }),可以等待已注册的处理程序完成。在处理程序内部处理每次读取载荷的失败,以免某次拒绝导致处理程序在其他读取仍在等待时结束。保留成功读取的参数值和原始读取错误,并在所有自有读取完成后报告。
目录结构
按功能领域组织测试。每个领域通常都有自己的 fixture 文件:
tests/playwright/
├── constants.ts # Base URLs, shared constants
├── fixtures/
│ ├── global.fixtures.ts # Extended test/expect (if project uses one)
│ ├── core-api.fixtures.ts # Shared API mock helpers
│ └── {feature}.fixtures.ts # Feature-specific test data
├── {feature-domain}/
│ └── {feature}.test.ts # Test file
└── routing/
└── redirects.test.ts # Router/redirect tests
编写测试文件
基本结构
import type { Page } from '@playwright/test';
import { test, expect } from '../fixtures/global.fixtures.ts';
import { mockEndpoint } from '../fixtures/api.fixtures.ts';
import { baseFixture, variantFixture } from '../fixtures/{feature}.fixtures.ts';
// Helper functions (navigation, assertions)
async function openFeaturePage(page: Page): Promise<void> {
await page.goto('/feature?id=test-id');
}
test.describe('Feature name', () => {
test('description of expected behavior', async ({ page }) => {
await mockEndpoint(page, baseFixture);
await openFeaturePage(page);
await expect(page.getByText('Expected text')).toBeVisible();
});
});
辅助函数
在测试文件顶部定义辅助函数,用于执行重复操作。辅助函数可提升可读性并减少重复:
- 导航辅助函数 —
openDialog(page)、gotoProductPage(page) - 断言辅助函数 —
expectSummary(page, options)、expectAvailability(page, from, to) - 设置辅助函数 —
setupCommonMocks(page),用于创建特定测试的模拟组合 - 定位器辅助函数 —
actionButtons(page),返回一个Locator
使用 beforeEach 进行共享设置
当 describe 块中的所有测试共享相同的模拟设置时:
test.describe('Router redirects', () => {
test.beforeEach(async ({ page }) => {
await setupCommonMocks(page);
});
test('redirects when resource is not found', async ({ page }) => {
await page.goto('/resource?id=unknown');
await expect(page).toHaveURL(/\/error\?id=unknown$/u);
});
});
时序与布局
- 使用 Playwright 断言等待相关 UI 状态。固定等待时间和
networkidle都无法证明待测元素或数据已就绪。 - 对于轮询或防抖行为,应在应用计时器启动前安装
page.clock,通常是在导航前。当场景需要执行每个计时器回调时,使用runFor;只有在跳过时间且减少回调次数符合待测行为时,才使用fastForward。 - 在触发操作或推进时钟前,注册请求或响应等待。推进时间,等待相关 HTTP 结果,并断言可见结果。保持应用的轮询、重新加载和状态变化按真实逻辑运行;更新模拟响应,而不是调用产品内部逻辑或直接设置最终 DOM 状态。
- 测量布局时,应等待相关字体加载完成且有限时长的元素动画结束。如果动画或过渡时序是测试约定的一部分,则保持动画启用,并观察所需的中间状态和最终状态。浏览器时钟不能替代 CSS 动画或服务器时间证据。
使用 test.step() 编写多步骤流程
对于购买、表单提交等复杂用户流程中的重要阶段,使用命名的 test.step() 代码块。这样,报告就能指出失败的阶段。除非项目有更具体的惯例,否则简短、连贯的测试应直接按顺序编写操作和断言。
test('completes purchase flow', async ({ page }) => {
await test.step('prepare mocks', async () => {
await mockProduct(page, productFixture);
await mockCheckout(page);
});
const purchaseRequestPromise = page.waitForRequest(
request => request.url().includes('/purchases') && request.method() === 'POST'
);
await test.step('open product page', async () => {
await gotoProductPage(page);
});
await test.step('select option', async () => {
await page.getByRole('button', { name: '14:00' }).click();
});
await test.step('verify confirmation page', async () => {
await expect.poll(() => new URL(page.url()).pathname).toBe('/order-confirmation');
});
await test.step('submit order', async () => {
await page.getByRole('button', { name: 'Confirm' }).click();
});
await test.step('verify purchase request', async () => {
const request = await purchaseRequestPromise;
expect(request.postDataJSON()).toEqual({
items: [{ id: 'product-1', date: '2088-04-21T11:00:00.000Z' }]
});
});
});
多步骤流程中的关键模式:
page.waitForRequest()— 在触发请求的操作之前设置expect.poll()— 在 SPA 导航后等待异步 URL 变化satisfies— 对请求负载预期进行类型检查,同时保留字面量类型
通过 addInitScript 设置 localStorage
要在页面加载前设置 localStorage 值(例如,为路由测试保存状态):
async function setSavedState(page: Page, key: string, value: unknown): Promise<void> {
await page.addInitScript(
({ storageKey, data }: { storageKey: string; data: string }) => {
localStorage.setItem(storageKey, data);
},
{ storageKey: key, data: JSON.stringify(value) }
);
}
addInitScript 会在页面加载前运行,因此应用会在初始化时读取正确的 localStorage 值。page.evaluate 会在页面加载后运行,对于路由守卫来说太迟了。
选择器模式
优先按以下顺序使用面向用户的选择器:
page.getByRole('button', { name: 'Confirm' })— 无障碍角色 + 名称page.getByText('Expected text')— 可见文本内容page.getByTestId('action-button')—data-testid属性page.getByText('text', { exact: true })— 精确匹配,避免部分匹配page.getByText(/regex pattern/u)— 用于动态内容的正则表达式
通过无障碍角色和名称、相关容器、行或卡片以及可见性,将定位器细化到目标元素。当位置本身属于待测行为,或测试已有充分理由按位置选择等效匹配项时,使用 .first() 或 .nth()。
断言模式
// Visibility
await expect(page.getByText('Welcome')).toBeVisible();
// Element count
await expect(page.getByRole('button', { name: /\d{2}:\d{2}/u })).toHaveCount(3);
await expect(page.getByText('Not present')).toHaveCount(0); // Assert absence
// URL matching
await expect(page).toHaveURL('http://localhost:5050/error?id=unknown');
await expect(page).toHaveURL(/\/order-success\?id=test-id$/u);
// Async URL change (SPA navigation)
await expect.poll(() => new URL(page.url()).pathname).toBe('/order-confirmation');
// Interactive state
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
// Unicode-aware regex with explicit apostrophe variants
await expect(page.getByText(/^You(?:'|’|ʼ)re all set!$/u)).toBeVisible();
始终使用 /u(Unicode)标志,以便正则表达式在 Unicode 感知模式下解析。如果文本中可能包含不同形式的撇号或其他字符变体,应在模式中明确列出可接受的变体。
面向用户的回归证据
- 选择断言之前,先定义需要保护的可观察行为。
- 使用相关的指针和键盘输入,测试每一种有实质变化的状态和转换。
- 如果受保护的行为被移除或绕过,回归测试必须失败。始终通过的测试无法保护该约定。
- 断言行为所需的渲染内容和几何布局。仅检查包装元素存在或
toBeVisible()仍可能通过,即使图标为空、子元素被裁切,或内容位于视口之外。 - 对于状态转换,断言相关的顺序、时机、焦点和稳定内容。仅检查最终状态,无法证明用户没有遇到闪烁、过时结果或焦点丢失。
- 对于响应式行为,选择能揭示不同故障的视口:检查溢出时使用支持的最小宽度,检查桌面布局时使用常见桌面宽度;如果行为因断点而异,则检查断点两侧。若容器响应式组件在窄、宽容器中存在不同风险,应在不改变视口的情况下分别测试。
- 当较长内容可能影响受保护的布局时,使用真实且较长的内容,以及不间断的长文本。检查文档和相关子元素的矩形;仅检查根元素的
scrollWidth,无法证明子元素可见或位于视口内。 - 断言范围应与约定一样精确。若只关心某个组件状态,不要用整页快照替代有针对性的证据。
使用 page.route() 模拟 API
基本模拟
async function mockProducts(page: Page, response: unknown): Promise<void> {
await page.route('**/api/products**', async (route) => {
await route.fulfill({ json: response });
});
}
模拟创建并轮询流程
async function mockPurchase(page: Page, purchaseId: string, response: unknown): Promise<void> {
// Mock POST (create)
await page.route('**/api/purchases', async (route) => {
await route.fulfill({ json: { id: purchaseId } });
});
// Mock GET (status poll)
await page.route(`**/api/purchases/${purchaseId}`, async (route) => {
await route.fulfill({ json: response });
});
}
控制无关的外部请求
根据场景和项目的网络策略选择网络边界。复用现有模拟,只覆盖场景所需的响应。使用 glob 或正则表达式,既可避免本地资源出现在来源限制器的拦截消息中,也可将其排除在处理器之外:
const appOrigin = new URL(appBaseUrl).origin;
const escapedAppOrigin = appOrigin.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
const externalRequests = new RegExp(`^(?!(?:blob:)?${escapedAppOrigin}(?:/|$))`, 'u');
await page.route(externalRequests, async (route) => {
await route.abort();
});
如果场景要测试外部集成本身,请使用配置好的测试环境,并保持该交互为真实交互。模拟被测行为之外的依赖项。
让每个模拟都重现与场景相关的生命周期,包括适用时的完成、过期、取消、重试或一次性值。不要让模拟产生真实依赖项无法产生的状态。如果无法安全地进行真实模拟,请使用官方测试模式,或将该行为标记为未经验证。
覆盖现有模拟
Playwright 的路由匹配采用后注册者优先。为相同模式注册新路由,即可覆盖较早的模拟:
// Global fixture mocks /api/properties with defaults
// Your test overrides with custom data:
await page.route('**/api/properties/**', async (route) => {
await route.fulfill({ json: customPropertyData });
});
验证请求取消
当取消行为很重要时,先挂起过时响应,在触发替代请求前注册 requestfailed,断言请求已中止(在 Chromium 中为 ERR_ABORTED),然后释放模拟并验证只渲染当前数据。最终 UI 本身无法证明传输已取消;绝不要完成已中止的路由。
模拟第三方依赖项
async function setupExternalMocks(page: Page): Promise<void> {
await page.route('https://analytics.example.com/**', async (route) => {
await route.fulfill({ status: 200, body: '' });
});
}
创建测试数据夹具
创建或修改夹具数据时,请阅读夹具指南。其中涵盖基础对象和变体、私有工厂、字面量类型、现有约定、场景日期和模拟辅助工具。
API 请求测试
如需直接测试服务端 API 端点,请使用 Playwright 的 APIRequestContext,无需浏览器。
用于身份验证的 Worker 作用域夹具
如果会话和服务器数据可以安全共享,请使用 Worker 作用域夹具进行 API 身份验证。夹具负责设置和清理,但其 APIRequestContext 仍会携带可变的 Cookie。对于会更改共享身份验证或服务器状态的测试,请使用单独的上下文或身份。
// tests/playwright/fixtures.ts
import { test as base, type APIRequestContext } from '@playwright/test'
import { appBaseUrl } from './constants'
interface WorkerFixtures {
authedRequest: APIRequestContext;
}
export const test = base.extend<Record<never, never>, WorkerFixtures>({
authedRequest: [
async ({ playwright }, use) => {
const request = await playwright.request.newContext({ baseURL: appBaseUrl })
await request.post('/api/auth/create-session')
await use(request)
await request.dispose()
},
{ scope: 'worker' },
],
})
在 API 测试文件中从此文件导入 test。在测试回调中,可通过 { authedRequest } 使用该夹具。
类型安全的 JSON 解析
APIResponse.json() 返回 Promise<any>(Playwright 的 Serializable = any)。测试读取 JSON 正文时,请使用 unknown 作为中间类型,并用 Valibot 解析:
const response = await request.get('/api/items')
const raw: unknown = await response.json()
const body = v.parse(mySchema, raw) // Valibot accepts unknown, returns typed result
Valibot 的 parse 会检查运行时结构并返回类型化数据。将请求、JSON 读取和解析分别放在单独的行上,以便清楚地定位失败。只检查状态、标头或正文缺失的测试,应使用相应的响应断言,而不读取 JSON。
API 测试中的断言
Valibot 验证响应结构后,应重点断言 HTTP 契约和被测行为。仅当架构已检查相同要求时,才移除结构断言:
// Redundant when v.parse(schema, raw) already validates this shape
expect(body).toMatchObject({ id: expect.any(Number), name: expect.any(String) })
// Tests the HTTP contract, correct data, filter logic, and auth behavior
expect(response.status()).toBe(200)
expect(body.name).toBe('MSR')
expect(body.items.length).toBeGreaterThan(0)
expect(filteredBody.items.every(item => item.category.slug === 'sleeping-pads')).toBe(true)
高价值的 API 断言:状态码、具体值(用于确认记录正确的名称、slug、ID)、筛选正确性、分页边界、身份验证执行情况(无会话时返回 401)。
测试 401 / 未通过身份验证的响应
内联创建匿名上下文——切勿在身份验证失败测试中复用已通过身份验证的 fixture:
test('returns 401 without session cookie', async ({ playwright }) => {
const anonRequest = await playwright.request.newContext({ baseURL: appBaseUrl })
const response = await anonRequest.get('/api/equipment/groups')
expect(response.status()).toBe(401)
await anonRequest.dispose()
})
# Run all Playwright tests
vpx playwright test
# Run a specific test file
vpx playwright test tests/playwright/product/checkout.test.ts
# Run with UI mode for debugging
vpx playwright test --ui
# Run with headed browser
vpx playwright test --headed
# Run with Playwright inspector/debug mode
vpx playwright test --debug
# Open the last HTML report
vpx playwright show-report
检查项目的 package.json,确认可用的测试脚本——许多项目会定义诸如 test:playwright、test:e2e 或类似的快捷脚本。
在项目相关的浏览器项目中运行已更改的浏览器场景,并禁用重试。确认服务器提供的是当前检出版本构建的内容;有关服务器复用和失败跟踪,请参阅配置指南。如果任务涉及测试套件运行时间、fixture 开销、worker 或分片,请阅读性能指南。普通测试更改无需反复进行全套基准测试。
验证清单
在完成 E2E 测试之前,请确认:
- 如果项目存在 fixture 文件,则从该文件导入
test和expect;否则从@playwright/test导入 - 诸如
Page和Locator的 Playwright 类型,均通过import type直接导入 - 特定功能的 fixture 对所有导出的对象使用
as const - 具有已知项目契约的 fixture 使用兼容的
satisfies检查;有意设为无效的 fixture 在其对应场景中仍保持无效 - fixture 日期代表预期状态;如果行为取决于当前时间或边界,则控制时间
-
test.describe()对相关测试进行分组 - 对重复的导航/断言模式提取辅助函数
- 复杂流程中的重要阶段使用
test.step()块;简短且连贯的测试保持直接 - 在触发操作之前设置
page.waitForRequest() - 正则表达式使用
/u标志 - 定位器以语义方式识别目标元素;
.first()或.nth()表示有意的位置选择 - 根据项目网络策略控制被测行为之外的依赖;被测集成在配置的测试环境中保持真实
- 读取 JSON 的 API 测试:正文以
unknown类型接收,并由 Valibot 验证;仅移除与架构重复的结构断言 - 不读取 JSON 的 API 测试:状态、标头和正文缺失情况使用相应的响应断言,不添加不必要的架构
- API 测试:身份验证测试使用内联匿名上下文,而非已通过身份验证的 fixture
- API 测试:身份验证使用由 fixture 管理、作用域可保留独立状态的上下文
- 测试成功运行
参考文件
- references/configuration.md — Playwright 配置设置:Web 服务器、报告器、跟踪、屏幕截图、CI 行为
- references/fixtures.md — 完整的 fixture 模式、工厂函数、变体模式详情
- references/performance.md — 衡量 fixture 开销、worker 和分片选择、耗时及资源使用情况