webapp-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb App Testing
Web应用测试
Overview
概述
Comprehensive web application testing using Playwright as the primary tool. This skill covers end-to-end testing workflows including screenshot capture for visual verification, browser console log analysis, user interaction simulation, visual regression testing, accessibility auditing with axe-core, network request mocking, and mobile viewport testing.
Announce at start: "I'm using the webapp-testing skill for Playwright-based web application testing."
以Playwright为核心工具的全面Web应用测试方案。该技能覆盖端到端测试全流程,包括用于视觉验证的截图捕获、浏览器控制台日志分析、用户交互模拟、视觉回归测试、基于axe-core的可访问性审计、网络请求模拟以及移动端视口测试。
开头提示: "我正在使用webapp-testing技能开展基于Playwright的Web应用测试。"
Phase 1: Test Planning
阶段1:测试规划
Goal: Identify what to test and set up the infrastructure.
目标: 明确测试范围并搭建基础环境。
Actions
操作步骤
- Identify critical user flows to test
- Define test environments and viewports
- Set up test fixtures and data
- Configure Playwright project settings
- Establish visual baseline screenshots
- 明确需要测试的核心用户流程
- 定义测试环境和视口参数
- 搭建测试夹具与测试数据
- 配置Playwright项目设置
- 建立视觉基线截图
User Flow Priority Decision Table
用户流程优先级决策表
| Flow Type | Priority | Test Depth |
|---|---|---|
| Authentication (login/logout/register) | Critical | Full happy + error paths |
| Core business workflow (purchase, submit) | Critical | Full happy + error + edge cases |
| Navigation and routing | High | All major routes |
| Search and filtering | High | Common queries + empty state |
| Settings and profile | Medium | Happy path |
| Admin/back-office | Medium | Key operations only |
| 流程类型 | 优先级 | 测试深度 |
|---|---|---|
| 身份验证(登录/登出/注册) | 最高 | 全量正向+异常路径 |
| 核心业务流程(购买、提交) | 最高 | 全量正向+异常+边界场景 |
| 导航与路由 | 高 | 所有核心路由 |
| 搜索与筛选 | 高 | 常用查询+空状态 |
| 设置与个人资料 | 中 | 正向路径 |
| 管理/后台系统 | 中 | 仅核心操作 |
STOP — Do NOT proceed to Phase 2 until:
暂停 — 满足以下条件前请勿进入阶段2:
- Critical user flows are identified and prioritized
- Test environments and viewports are defined
- Playwright config is ready
- Test data strategy is defined
- 核心用户流程已梳理完成并划分优先级
- 测试环境与视口参数已定义
- Playwright配置已就绪
- 测试数据策略已明确
Phase 2: Test Implementation
阶段2:测试实现
Goal: Write tests using page object models and accessible locators.
目标: 使用页面对象模型与可访问性定位器编写测试用例。
Actions
操作步骤
- Write page object models for key pages
- Implement end-to-end test scenarios
- Add visual regression snapshots
- Integrate accessibility checks
- Configure network mocking for isolated tests
- 为核心页面编写页面对象模型
- 实现端到端测试场景
- 添加视觉回归快照
- 集成可访问性检查
- 配置网络模拟实现测试隔离
Playwright Configuration
Playwright配置
typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});Page Object Model
页面对象模型
typescript
class LoginPage {
constructor(private page: Page) {}
readonly emailInput = this.page.getByLabel('Email');
readonly passwordInput = this.page.getByLabel('Password');
readonly submitButton = this.page.getByRole('button', { name: 'Sign in' });
readonly errorMessage = this.page.getByRole('alert');
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}typescript
class LoginPage {
constructor(private page: Page) {}
readonly emailInput = this.page.getByLabel('Email');
readonly passwordInput = this.page.getByLabel('Password');
readonly submitButton = this.page.getByRole('button', { name: 'Sign in' });
readonly errorMessage = this.page.getByRole('alert');
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}Locator Selection Decision Table
定位器选择决策表
| Locator Type | Priority | When to Use |
|---|---|---|
| 1st choice | Any element with ARIA role (button, link, heading) |
| 2nd choice | Form fields with labels |
| 3rd choice | Fields without visible labels |
| 4th choice | Non-interactive visible text |
| Last resort | When no accessible locator works |
| CSS selector / XPath | Never | Breaks with styling changes |
| 定位器类型 | 优先级 | 适用场景 |
|---|---|---|
| 首选 | 所有带ARIA角色的元素(按钮、链接、标题) |
| 次选 | 带标签的表单字段 |
| 第三选择 | 无可见标签的字段 |
| 第四选择 | 非交互类可见文本 |
| 最后方案 | 无可用可访问性定位器时使用 |
| CSS selector / XPath | 禁止使用 | 样式变更会导致定位失效 |
STOP — Do NOT proceed to Phase 3 until:
暂停 — 满足以下条件前请勿进入阶段3:
- Page object models exist for key pages
- Tests use accessible locators exclusively
- Visual baselines are established
- Accessibility checks are integrated
- Network mocking is configured for isolated tests
- 核心页面的页面对象模型已创建完成
- 测试用例仅使用可访问性定位器
- 视觉基线已建立
- 可访问性检查已集成
- 已配置网络模拟实现测试隔离
Phase 3: CI Integration
阶段3:CI集成
Goal: Configure reliable, fast test execution in CI.
目标: 在CI环境中配置稳定、高效的测试执行流程。
Actions
操作步骤
- Configure headless browser execution
- Set up screenshot artifact collection
- Configure retry and flake detection
- Add reporting (HTML report, JUnit XML)
- Set up visual diff review process
- 配置无头浏览器执行模式
- 设置截图制品收集规则
- 配置重试与不稳定测试检测机制
- 添加报告能力(HTML报告、JUnit XML)
- 建立视觉差异审核流程
CI Configuration Checklist
CI配置检查清单
- Tests run headless in CI
- Retries enabled (2 retries for CI)
- Screenshot and video artifacts collected on failure
- JUnit XML output for CI integration
- HTML report generated for manual review
- Visual diff snapshots reviewed before merge
- 测试在CI中以无头模式运行
- 已启用重试机制(CI环境重试2次)
- 失败时自动收集截图和视频制品
- 输出JUnit XML用于CI集成
- 生成HTML报告供人工审核
- 合并前已审核视觉差异快照
STOP — CI integration complete when:
暂停 — 满足以下条件则CI集成完成:
- Tests run reliably in CI pipeline
- Artifacts are collected on failure
- Flaky tests are identified and fixed (not skipped)
- 测试在CI流水线中稳定运行
- 失败时制品已自动收集
- 不稳定测试已识别并修复(而非跳过)
Screenshot Capture Patterns
截图捕获模式
Full Page
全页截图
typescript
test('homepage renders correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});typescript
test('homepage renders correctly', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});Element-Level
元素级截图
typescript
test('navigation bar matches design', async ({ page }) => {
await page.goto('/');
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot('navbar.png');
});typescript
test('navigation bar matches design', async ({ page }) => {
await page.goto('/');
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot('navbar.png');
});Dynamic Content Masking
动态内容屏蔽
typescript
test('dashboard layout', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.locator('[data-testid="timestamp"]'),
page.locator('[data-testid="user-avatar"]'),
page.locator('.chart-container'),
],
animations: 'disabled',
});
});typescript
test('dashboard layout', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.locator('[data-testid="timestamp"]'),
page.locator('[data-testid="user-avatar"]'),
page.locator('.chart-container'),
],
animations: 'disabled',
});
});Browser Log Analysis
浏览器日志分析
typescript
test('no console errors on page load', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
page.on('pageerror', error => {
consoleErrors.push(error.message);
});
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(consoleErrors).toEqual([]);
});typescript
test('no console errors on page load', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
page.on('pageerror', error => {
consoleErrors.push(error.message);
});
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(consoleErrors).toEqual([]);
});Accessibility Testing with axe-core
基于axe-core的可访问性测试
typescript
import AxeBuilder from '@axe-core/playwright';
test('page has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('.third-party-widget')
.analyze();
expect(results.violations).toEqual([]);
});typescript
import AxeBuilder from '@axe-core/playwright';
test('page has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.exclude('.third-party-widget')
.analyze();
expect(results.violations).toEqual([]);
});Network Request Mocking
网络请求模拟
typescript
test('displays users from API', async ({ page }) => {
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
});
test('handles API errors gracefully', async ({ page }) => {
await page.route('**/api/users', route =>
route.fulfill({ status: 500, body: 'Internal Server Error' })
);
await page.goto('/users');
await expect(page.getByText('Something went wrong')).toBeVisible();
});typescript
test('displays users from API', async ({ page }) => {
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
});
test('handles API errors gracefully', async ({ page }) => {
await page.route('**/api/users', route =>
route.fulfill({ status: 500, body: 'Internal Server Error' })
);
await page.goto('/users');
await expect(page.getByText('Something went wrong')).toBeVisible();
});Mobile Viewport Testing
移动端视口测试
typescript
test.describe('mobile responsive', () => {
test.use({ viewport: { width: 375, height: 667 } });
test('hamburger menu works', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).not.toBeVisible();
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
});
});typescript
test.describe('mobile responsive', () => {
test.use({ viewport: { width: 375, height: 667 } });
test('hamburger menu works', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).not.toBeVisible();
await page.getByRole('button', { name: 'Menu' }).click();
await expect(page.getByRole('navigation')).toBeVisible();
});
});Test Organization
测试目录结构
tests/
e2e/
auth/
login.spec.ts
register.spec.ts
checkout/
cart.spec.ts
payment.spec.ts
fixtures/
test-data.ts
auth.setup.ts
pages/
login.page.ts
dashboard.page.ts
utils/
helpers.tstests/
e2e/
auth/
login.spec.ts
register.spec.ts
checkout/
cart.spec.ts
payment.spec.ts
fixtures/
test-data.ts
auth.setup.ts
pages/
login.page.ts
dashboard.page.ts
utils/
helpers.tsAnti-Patterns / Common Mistakes
反模式/常见错误
| Anti-Pattern | Why It Is Wrong | Correct Approach |
|---|---|---|
| CSS selectors or XPath | Break with styling changes | Use accessible locators (role, label, text) |
| Arbitrary delays, flaky | Use |
| Testing third-party components in detail | Not your code to test | Test your integration, not their internals |
| Hardcoded test data | Breaks across environments | Use fixtures and factories |
| Tests depending on execution order | Fragile, hard to debug | Each test must be independent |
| Ignoring flaky tests | Erodes trust in test suite | Fix root cause or quarantine |
| Screenshots without masking dynamic content | Always different, always failing | Mask timestamps, avatars, charts |
| No accessibility checks | Missing critical quality gate | axe-core on every page |
| 反模式 | 问题原因 | 正确做法 |
|---|---|---|
| CSS选择器或XPath | 样式变更会导致定位失效 | 使用可访问性定位器(role、label、text) |
| 固定延迟不稳定,易导致测试flaky | 使用 |
| 详细测试第三方组件 | 不属于你的代码测试范围 | 仅测试集成逻辑,不测试第三方组件内部实现 |
| 硬编码测试数据 | 跨环境易失效 | 使用夹具与数据工厂 |
| 测试依赖执行顺序 | 脆弱,难调试 | 每个测试用例必须独立 |
| 忽略不稳定测试 | 会降低测试套件可信度 | 修复根因或隔离处理 |
| 截图未屏蔽动态内容 | 内容每次都不同,测试永远失败 | 屏蔽时间戳、头像、图表等动态内容 |
| 无可访问性检查 | 缺失关键质量门禁 | 每个页面都要跑axe-core检测 |
Integration Points
集成关联
| Skill | Relationship |
|---|---|
| Frontend components are tested by E2E tests |
| E2E tests are the top of the testing pyramid |
| User flow tests serve as acceptance tests |
| Performance budgets can be verified in E2E |
| Review checks that tests use accessible locators |
| Security headers and auth flows tested in E2E |
| 技能 | 关联关系 |
|---|---|
| 前端组件由E2E测试覆盖 |
| E2E测试是测试金字塔的顶层 |
| 用户流程测试可作为验收测试 |
| 性能指标可在E2E测试中验证 |
| 代码评审检查测试是否使用可访问性定位器 |
| 安全头与鉴权流程可在E2E中测试 |
Quality Checklist
质量检查清单
- All critical user flows covered
- Tests use accessible locators (role, label, text)
- Network mocking for isolated tests
- Visual regression baselines reviewed and approved
- Accessibility scans on all pages
- Mobile viewport tests for responsive features
- No (use proper assertions)
waitForTimeout - CI pipeline configured with retries
- Screenshot artifacts collected on failure
- Flaky tests identified and fixed (not skipped)
- 所有核心用户流程已覆盖
- 测试使用可访问性定位器(role、label、text)
- 配置网络模拟实现测试隔离
- 视觉回归基线已审核通过
- 所有页面已完成可访问性扫描
- 响应式功能已覆盖移动端视口测试
- 无(使用合理断言替代)
waitForTimeout - CI流水线已配置重试机制
- 失败时自动收集截图制品
- 不稳定测试已识别并修复(而非跳过)
Skill Type
技能类型
FLEXIBLE — Adapt test depth to the project's critical paths. The page object model pattern and accessible locators are strongly recommended. Accessibility checks are mandatory on every page. Visual regression baselines must be reviewed before merge.
灵活适配 — 根据项目核心路径调整测试深度。强烈建议使用页面对象模型模式与可访问性定位器。所有页面必须强制进行可访问性检查。视觉回归基线必须在合并前完成审核。