关于 playwright-testing
playwright-testing 是一项为 Playwright 端到端测试提供编写模式的技能,面向需要提高测试可靠性的开发者。它强调完整覆盖边界与错误场景、基于条件等待,以及按测试目标选择模拟策略。
主要功能
- 01覆盖正常、错误及边界场景
- 02使用条件等待代替固定超时
- 03按测试意图选择 API 模拟
- 04用 page.locator() 定位元素
- 05拦截网络并验证请求契约
适用场景
- 01编写发布前的端到端测试
- 02减少 CI 中的间歇性失败
- 03验证错误处理与重试逻辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zenobi-us/ dotfiles --skill playwright-testing安装后在 Claude Code 中输入/playwright-testing确认已加载。
详细介绍
使用 Playwright 进行测试
概述
Playwright 测试会以三种可预见的方式失败:覆盖不完整(未考虑边界情况就发布)、等待方式脆弱(固定超时导致测试偶发失败),以及模拟方式不明确(默认全部模拟或全部使用预发布环境)。本技能提供了在时间紧迫时编写可靠测试的模式。
核心原则:发布前完成覆盖。始终使用确定性等待。根据测试意图有策略地进行模拟。
适用场景
表明你需要此技能的迹象:
- 临近部署截止时间,正在编写端到端测试
- 测试使用带固定值的
waitForSelector()或waitForTimeout() - 不确定该模拟 API,还是针对预发布环境进行测试
- 测试在本地通过,但在 CI 中偶发失败
- 边界情况覆盖被推迟到“下个冲刺”
关键模式:发布前完成覆盖
面临的压力: 测试已经能正常运行。截止时间还有 30 分钟。检查边界情况需要 15 分钟。现在发布?
答案: 不要。只有满足以下条件才发布:
- 正常流程测试通过 ✓
- 已测试错误情况(网络故障、超时、缺少数据)
- 已验证重试逻辑
- 已处理边界条件
顺序为何重要: 测试覆盖不完整会导致隐藏缺陷。推迟边界情况检查意味着部署未经测试的故障路径。用户会最先遇到这些问题。
没有例外:
- 不能说“下个冲刺再加”(你不会加)
- 不能说“边界情况不太可能发生”(它们会在生产环境中发生)
- 不能说“正常流程测试已经够好了”(并不够)
等待策略:基于条件,而非时间
问题: 使用 page.waitForSelector() 等固定超时(默认 5 秒):
- 在快速系统上很慢(不必要的等待)
- 在慢速系统上不稳定(超时时间过短)
- 掩盖实际问题(你在等什么?)
- 容易让人把重构推迟(“之后再修”)
模式:
// ❌ BAD: Fixed timeout, hides what you're waiting for
await page.waitForSelector('.loading', { timeout: 5000 });
await page.click('button');
// ✅ GOOD: Explicit condition, timeout is safety net
// Wait for loading spinner to appear, proving async work started
await page.locator('.loading').waitFor({ state: 'visible' });
// Wait for it to disappear, proving async work completed
await page.locator('.loading').waitFor({ state: 'hidden' });
await page.click('button');
// ✅ GOOD: Custom condition when standard waits don't fit
async function waitForApiCall(page: Page, method: string) {
let apiCalled = false;
page.on('response', (response) => {
if (response.request().method() === method) {
apiCalled = true;
}
});
// Keep checking until API was called
await page.waitForFunction(() => apiCalled);
}
这为何重要: 基于条件的等待能明确你要测试的内容。在快速系统上更快,在慢速系统上更可靠,并且能立即发现时序问题,而不是等到超时才发现。
适用于:
- DOM 状态变化(可见/隐藏/已附加)
- API 调用(网络拦截)
- 数据更新(文本内容变化)
- 表单就绪状态(按钮启用/禁用)
关键: 不要把基于条件的等待推迟到“未来重构”。使用固定超时的测试会:
- 在本地通过(机器速度快)
- 在 CI 中偶发失败(负载较高)
- 一直不稳定(之后的重构永远不会发生)
行动: 基于条件的等待只需多写 2 行。现在就写。不要“以后再写”,不要“改到这个文件时再写”,也不要“等出现缺陷再写”。现在就写。
仅将超时作为安全兜底:
// Reasonable defaults: 5s for navigation, 10s for complex async
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
await page.locator('[data-testid]').waitFor({ timeout: 10000 });
模拟策略:基于意图,而非非此即彼
问题: 两种极端做法都会失败:
- 全部模拟: 测试通过,但无法发现集成缺陷(模拟结果与真实行为不符)
- 全部使用预发布环境: 测试会因基础设施不稳定而偶发失败(预发布环境是真实的,但不可靠)
模式:根据测试意图采用混合策略
// INTENT: Test UI logic, not API integration
// → Mock the API, test DOM updates
test('displays user data when loaded', async ({ page }) => {
await page.route('/api/user', route => {
route.abort(); // Simulate network failure
});
await page.goto('/profile');
await expect(page.locator('.error-message')).toContainText('Failed to load');
});
// INTENT: Test API integration, not UI
// → Hit staging, verify contract is correct
test('payment endpoint returns correct schema', async ({ page }) => {
// Hit real staging, prove response matches what UI expects
const response = await page.request.post(
`${process.env.STAGING_API}/payment`,
{ data: { amount: 100 } }
);
expect(response.ok()).toBeTruthy();
const json = await response.json();
expect(json).toHaveProperty('transactionId');
expect(json).toHaveProperty('status');
});
// INTENT: Test complete critical flow
// → Hybrid: mock non-critical paths, hit staging for critical ones
test('checkout flow succeeds end-to-end', async ({ page }) => {
// Mock product catalog (doesn't change)
await page.route('/api/products', route => {
route.continue({ response: mockProducts });
});
// Hit real staging for payment (critical + mature)
// Hit real staging for order confirmation (critical + stable)
// Results in fast + reliable + safe tests
});
决策树:
- UI 逻辑测试(你测试的 99%)→ 模拟 API,测试 UI 响应
- 契约测试(测试的 1-2%)→ 针对关键集成使用预发布环境
- 预发布环境不稳定? → 增加模拟,改为测试 UI 的弹性
- 覆盖有缺口? → 为预发布环境不易触发的错误情况添加模拟场景
常见错误与自我辩解
| 自我辩解 | 事实 | 行动 |
|---|---|---|
| “我会把边缘情况测试留到下个迭代” | 你不会。边缘情况总是未经测试就发布。 | 覆盖完整再发布,否则就别发布。没有例外。 |
| “固定超时足够好” | 它们在本地能用,在 CI 中却会间歇性失败。基于条件的等待并不更难。 | 使用基于条件的等待。别说“以后”。就现在。 |
| “我会边做边把等待改成基于条件的” | 你不会。固定超时会一直留着。CI 也会一直间歇性失败。 | 先编写基于条件的等待。别说“下次”。就这次。 |
| “手动测试能覆盖边缘情况” | 不能。手动测试无法防止回归。 | 自动化边缘情况测试是强制要求。两者都很重要。 |
| “全部使用模拟很务实” | 务实意味着测试能正常运行。全部使用模拟会掩盖集成错误。 | 针对关键路径在预发布环境中测试。其余部分按需模拟。 |
| “我的配置太复杂,现在没法重构” | 并非如此。基于条件的等待只需 2 行。所谓复杂只是借口。 | 先编写正确的等待。为发布而改变行为,而不是为了方便。 |
危险信号 - 停止
如果你在说以下内容,请停下来重新开始:
- “等超时出现间歇性失败时我再修”
- “边缘情况可以先发布,以后再完善”
- “现在这样够好了”
- “我的配置不一样”
- “我这是在务实”
这些都意味着:测试覆盖不完整,加上脆弱的等待逻辑,将会被发布。删除并重写,确保覆盖完整,并使用基于条件的等待。
实现
使用 page.locator()(优先)而非 page.$(),以获得:
- 内置等待
- 更清晰的错误消息
- 代码意图一目了然
// Find element and wait for it to be visible
await page.locator('[data-testid="submit"]').waitFor({ state: 'visible' });
// Find and click in one step (waits for visibility first)
await page.locator('[data-testid="submit"]').click();
以清晰明确的方式路由 API:
// All error cases for this path
await page.route('/api/checkout/**', route => {
if (Math.random() > 0.8) route.abort(); // 20% failure rate
else route.continue();
});
拦截网络请求以验证契约:
const requests: any[] = [];
await page.on('request', (request) => {
if (request.url().includes('/api')) {
requests.push({
url: request.url(),
method: request.method(),
postData: request.postData(),
});
}
});
// After test actions
expect(requests).toContainEqual(
expect.objectContaining({ method: 'POST', url: expect.stringContaining('/payment') })
);
实际影响
将这些模式应用于 zenobi.us E2E 测试套件后:
- 固定超时导致的间歇性失败率从 8% 降至 0.3%(基于条件的等待)
- 边缘情况覆盖率从 42% 提高到 94%(发布前确保完整)
- 测试维护工作减少了 60%(意图更清晰,莫名失败更少)
- 模拟策略在提升预发布环境集成信心的同时,将 CI 耗时缩短了 35%