Web E2E 测试专家技能
专注 Vue/React 前端项目的浏览器端到端测试能力。
基于 Playwright + AI Agent 实现从测试计划生成 → 代码生成 → 失败修复的完整闭环。
触发条件
当用户提到以下关键词时触发此 Skill:
- "Vue/React 组件测试" / "web component test"
- "浏览器自动化测试" / "browser automation"
- "Playwright 测试" / "E2E 测试"
- "视觉回归测试" / "visual regression"
- "Flaky test 治理" / "不稳定测试修复"
技术栈依赖
核心 CLI
# 必须安装
npm install -D @playwright/test
# 可选增强
npm install -D vitest # 单元测试补充
AI Agent 初始化
npx playwright init-agents --loop=claude
# 或
npx playwright init-agents --loop=codex
功能范围
Phase 1: 测试场景识别与计划生成
输入:
- 应用 URL(本地开发环境)
- (可选)PRD/User Story 描述
- (可选)现有测试代码模式参考
输出:
specs/features/*.md— 功能测试计划(Gherkin 格式)specs/regression.md— 视觉回归检查清单specs/performance.md— 性能基准要求
工作流程:
Step 1.1: UI 探索分析
// AI 自动打开浏览器扫描应用
const page = await context.newPage();
await page.goto('http://localhost:3000');
// 提取关键元素
const elements = await page.locator('button, input, a').all();
const routes = await extractRoutes(page);
输出物:
# UI Structure Analysis
- 路由表:`/, /login, /dashboard, /settings`
- 主要组件:登录表单、导航栏、数据表格、操作按钮
- 交互点:47 个可点击元素,12 个表单字段
Step 1.2: 生成测试计划
# specs/features/user-authentication.md
Feature: User Authentication
As a registered user
I want to log in securely
So that I can access my account
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter valid email and password
And I click "Sign In" button
Then I should see the dashboard
And I should receive a welcome message
Scenario Outline: Login validation
When I enter "<email>" as email
And I submit the form
Then I should see error "<error_message>"
Examples:
| email | error_message |
| invalid | Invalid email format|
| empty@ | Invalid email format|
| | Email is required |
Phase 2: Playwright 测试代码生成
输入:
specs/features/*.md测试计划tests/seed.spec.ts种子测试模板
输出:
tests/features/auth/login.spec.tstests/regression/*.spec.tstests/e2e/*.spec.ts
Step 2.1: 种子测试初始化
// tests/seed.spec.ts
import { test, expect } from '@playwright/test';
test.use({
baseURL: process.env.TEST_BASE_URL || 'http://localhost:3000',
viewport: { width: 1280, height: 720 },
});
test.describe.configure({ mode: 'setup' });
test('prepare authentication fixture', async ({ page }) => {
await page.goto('/login');
// Verify page loads correctly
await expect(page.getByLabel(/email|username/i)).toBeVisible();
await expect(page.getByLabel(/password/i)).toBeVisible();
// Save login utility for other tests
await page.addInitScript(() => {
window.__TEST_UTILS__ = {
goToLogin: async () => {
await window.location.assign('/login');
},
getCurrentUser: () => localStorage.getItem('user')
};
});
});
test('seed: ensure clean storage state', async ({ page, context }) => {
await page.goto('/logout');
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
});
Step 2.2: 生成特性测试
// tests/features/auth/login.spec.ts
import { test, expect } from '../../fixtures';
import { TestDataFactory } from '../../utils/data-factory';
test.describe('User Authentication', () => {
let testUser: ReturnType<typeof TestDataFactory.createUser>;
test.beforeEach(async () => {
// Create unique test user for each scenario
testUser = TestDataFactory.createUser('login-test');
// Seed data via API
await test.request.post('/api/users', {
data: testUser
});
});
test('should login successfully with valid credentials', async ({ page }) => {
// ✅ Semantic selectors (stable against CSS changes)
const emailInput = page.getByLabel(/email/i);
const passwordInput = page.getByLabel(/password/i);
const submitButton = page.getByRole('button', { name: /sign in/i });
// ✅ Fill form
await emailInput.fill(testUser.email);
await passwordInput.fill(testUser.password);
// ✅ Click submit
await submitButton.click();
// ✅ Assert success
await expect(page.getByText(/welcome|dashboard/i)).toBeVisible();
await expect(page).toHaveURL(/.*dashboard.*/);
});
test('should show validation errors for invalid inputs', async ({ page }) => {
const testCases = [
{ email: 'invalid', expected: 'Invalid email format' },
{ email: '', expected: 'Email is required' }
];
for (const { email, expected } of testCases) {
await page.goto('/login');
await page.getByLabel(/email/i).fill(email);
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page.getByText(expected)).toBeVisible();
}
});
test('should redirect to previous page after successful login', async ({ page }) => {
// Navigate to protected route first
await page.goto('/dashboard');
// Should be redirected to login with next parameter
await expect(page).toHaveURL(/.*login.*next=.*/);
// Login
await page.getByLabel(/email/i).fill(testUser.email);
await page.getByLabel(/password/i).fill(testUser.password);
await page.getByRole('button', { name: /sign in/i }).click();
// Should redirect back to original destination
await expect(page).toHaveURL('/dashboard');
});
});
Step 2.3: 最佳实践注入
| 原则 | 示例 | 说明 |
|---|---|---|
| 语义选择器 | getByRole, getByLabel |
不依赖 CSS class |
| 显式等待 | waitForLoadState('networkidle') |
非硬编码 delay |
| 数据驱动 | test.each([inputs]) |
避免重复代码 |
| 测试隔离 | beforeEach清理状态 |
互不干扰 |
| Fixture 复用 | auth, dataFactory |
DRY 原则 |
Phase 3: Healer 失败诊断与修复
输入:
- 失败的测试结果
- Screenshot/Trace 文件
输出:
- 修复建议报告
- (可选)直接提交修复 commit
Step 3.1: 失败分类(Classifier)
async function classifyFailure(errorLog: string): Promise<TestFailureType> {
if (errorLog.includes('Element not found')) {
return {
type: 'UI_ELEMENT_CHANGED',
severity: 'MEDIUM',
fixStrategy: 'UPDATE_LOCATOR',
explanation: 'DOM 结构变更,需要更新选择器'
};
}
if (errorLog.includes('Timeout') && errorLog.includes('waiting')) {
return {
type: 'ASYNCHRONOUS_OPERATION',
severity: 'LOW',
fixStrategy: 'ADD_WAIT_OR_RETRY',
explanation: '异步操作未完成,需要智能等待'
};
}
if (errorLog.includes('Expectation failed')) {
return {
type: 'ASSERTION_MISMATCH',
severity: 'HIGH',
fixStrategy: 'REVIEW_ASSERTION',
explanation: '断言与实际结果不一致,需检查逻辑'
};
}
return { type: 'UNKNOWN', severity: 'CRITICAL', fixStrategy: 'MANUAL_REVIEW' };
}
Step 3.2: 自动修复策略
Case 1: UI 元素变更
// ❌ OLD - Fragile selector
await page.click('.btn-primary.submit-button.v-btn--contained');
// ✅ NEW - Semantic selector (稳定)
await page.getByRole('button', { name: /submit/i }).click();
Case 2: 异步操作延迟
// ❌ WRONG - Magic number
await page.click('#submit');
await new Promise(r => setTimeout(r, 3000));
// ✅ CORRECT - Smart wait
await page.click('#submit');
await page.waitForResponse(
resp => resp.url().includes('/api/submit') && resp.status() === 200
);
await expect(page.getByText('Success')).toBeVisible();
Case 3: Flaky Test
// Mark as flaky + Add retry logic
test.describe.configure({ retries: 2 });
test('should handle concurrent operations (FLAKY)', async ({ page }) => {
test.info().annotations.push({
type: 'flaky',
description: 'Intermittent failures due to race condition, tracked in issue #1234'
});
// Retry-friendly implementation
for (let i = 0; i < 3; i++) {
try {
await performOperation();
break;
} catch (error) {
if (i === 2) throw error;
await page.waitForTimeout(1000 * (i + 1));
}
}
});
Phase 4: 视觉回归测试
配置:
// playwright.config.ts
export default defineConfig({
screenshot: {
mode: 'only-on-failure', // 失败时自动截图
fullPage: true,
transitions: 'allow', // 捕获过渡动画
},
// Visual testing plugin (可选)
use: {
clip: { x: 0, y: 0, width: 1280, height: 720 },
},
});
执行流程:
# First run: Create baseline
npx playwright test --grep "@visual" --update-snapshots
# Subsequent runs: Compare against baseline
npx playwright test --grep "@visual"
报告输出:
Visual Regression Report
✅ Homepage - No differences detected
⚠️ Dashboard - 3% pixel difference (check screenshots/)
❌ Settings Page - Layout shift detected
Phase 5: Flaky Test 检测与治理
自动检测:
# Run tests multiple times to detect flakiness
npx playwright test --repeat-each=5 --workers=1
治理策略:
- 隔离标记 →
test.fail()临时禁用 - 根本原因分析 → 日志 + Trace 文件审查
- 修复策略 → 根据分类选择对应方案
- 回归检查 → 确保没有引入新问题
参考资源
模板库
最佳实践
高级主题
与父级 Skill 集成
devlab-test-onboard 调用方式
用户:"新项目需要配置测试"
↓
devlab-test-onboard 检测到项目特征:Vue + Vite
↓
自动推荐:devlab-web-test-e2e
↓
执行命令:
1. npm install -D @playwright/test
2. npx playwright init-agents --loop=claude
3. 生成基础测试模板
引用共享资源
<!-- 在测试代码中添加引用注释 -->
/**
* Fixture Library Reference
* Source: devlab-test-fixture-lib/fixtures/playwright-base.ts
* See also: https://github.com/team/test-fixture-lib
*/
import { baseTest, expect } from './fixtures';
探索性测试委托提示词模板(AI Agent 自主 E2E)
来源:Windsurf 会话挖掘(scan-2026-07-27-073234),AI Agent 自主跑 Playwright 探索测试实战验证。
委托 AI Agent 做探索性 E2E 测试时,提示词预埋以下绕行手法,避免 Agent 卡死:
| 障碍 | 绕行手法 |
|---|---|
| 点击目标被遮挡(overlay/toast) | 改点关联 label / 用键盘导航 / 先关掩层再点 |
| 元素 detach(重渲染后 stale) | 重新定位后重试,不复用旧 handle |
| 弹窗/确认框打断流程 | 提前声明 dialog 处理策略(accept/dismiss) |
| 异步加载未完成 | 断言前等待关键元素可见而非固定 sleep |
委托提示词骨架:
探索测试 <页面/功能>:逐个交互元素操作并验证反馈。
约束:点击被遮挡时改点 label 或先关掩层;元素 detach 时重新定位重试(最多 2 次);
每步截图留证;遇阻塞记录障碍+尝试过的绕行手法后继续下一项,不卡死在单点。
输出:通过项/失败项/阻塞项三段清单。
外层 SDD 框架衔接(能力级,见 devlab-harness-ops Step 7)
本技能是 Web E2E 用例生成/修复的 domain floor。功能开发接缝处的 red-green-refactor TDD 纪律由外层 SDD 框架的 tdd skill(如 mattpocock tdd / comet Classic build)驱动(命中则在测试接缝调用);trellis 类轻量框架或无框架时,本技能独立完成 E2E 层,能力不落空(floor 不可裁)。
已知限制
- ⚠️ 不支持纯后端测试(请使用
devlab-srv-test-api) - ⚠️ 不支持多浏览器并发调试(需手动配置
--headed) - ✅ 支持所有主流现代浏览器(Chrome, Firefox, Safari)
扩展阅读
- Playwright Official Documentation
- QA Wolf Best Practices
- Antfu Vue Testing Handbook
- Testing Library Philosophy
Managed by Trellis. This skill follows the harness-ai-kit asset pattern for web E2E testing specialization.
Last updated: 2026-07-22
Author: AI-assisted implementation based on enterprise best practices.