webapp-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web 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

操作步骤

  1. Identify critical user flows to test
  2. Define test environments and viewports
  3. Set up test fixtures and data
  4. Configure Playwright project settings
  5. Establish visual baseline screenshots
  1. 明确需要测试的核心用户流程
  2. 定义测试环境和视口参数
  3. 搭建测试夹具与测试数据
  4. 配置Playwright项目设置
  5. 建立视觉基线截图

User Flow Priority Decision Table

用户流程优先级决策表

Flow TypePriorityTest Depth
Authentication (login/logout/register)CriticalFull happy + error paths
Core business workflow (purchase, submit)CriticalFull happy + error + edge cases
Navigation and routingHighAll major routes
Search and filteringHighCommon queries + empty state
Settings and profileMediumHappy path
Admin/back-officeMediumKey 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

操作步骤

  1. Write page object models for key pages
  2. Implement end-to-end test scenarios
  3. Add visual regression snapshots
  4. Integrate accessibility checks
  5. Configure network mocking for isolated tests
  1. 为核心页面编写页面对象模型
  2. 实现端到端测试场景
  3. 添加视觉回归快照
  4. 集成可访问性检查
  5. 配置网络模拟实现测试隔离

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 TypePriorityWhen to Use
getByRole
1st choiceAny element with ARIA role (button, link, heading)
getByLabel
2nd choiceForm fields with labels
getByPlaceholder
3rd choiceFields without visible labels
getByText
4th choiceNon-interactive visible text
getByTestId
Last resortWhen no accessible locator works
CSS selector / XPathNeverBreaks with styling changes
定位器类型优先级适用场景
getByRole
首选所有带ARIA角色的元素(按钮、链接、标题)
getByLabel
次选带标签的表单字段
getByPlaceholder
第三选择无可见标签的字段
getByText
第四选择非交互类可见文本
getByTestId
最后方案无可用可访问性定位器时使用
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

操作步骤

  1. Configure headless browser execution
  2. Set up screenshot artifact collection
  3. Configure retry and flake detection
  4. Add reporting (HTML report, JUnit XML)
  5. Set up visual diff review process
  1. 配置无头浏览器执行模式
  2. 设置截图制品收集规则
  3. 配置重试与不稳定测试检测机制
  4. 添加报告能力(HTML报告、JUnit XML)
  5. 建立视觉差异审核流程

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

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

Anti-Patterns / Common Mistakes

反模式/常见错误

Anti-PatternWhy It Is WrongCorrect Approach
CSS selectors or XPathBreak with styling changesUse accessible locators (role, label, text)
page.waitForTimeout()
Arbitrary delays, flakyUse
expect().toBeVisible()
or similar
Testing third-party components in detailNot your code to testTest your integration, not their internals
Hardcoded test dataBreaks across environmentsUse fixtures and factories
Tests depending on execution orderFragile, hard to debugEach test must be independent
Ignoring flaky testsErodes trust in test suiteFix root cause or quarantine
Screenshots without masking dynamic contentAlways different, always failingMask timestamps, avatars, charts
No accessibility checksMissing critical quality gateaxe-core on every page

反模式问题原因正确做法
CSS选择器或XPath样式变更会导致定位失效使用可访问性定位器(role、label、text)
page.waitForTimeout()
固定延迟不稳定,易导致测试flaky使用
expect().toBeVisible()
或类似断言
详细测试第三方组件不属于你的代码测试范围仅测试集成逻辑,不测试第三方组件内部实现
硬编码测试数据跨环境易失效使用夹具与数据工厂
测试依赖执行顺序脆弱,难调试每个测试用例必须独立
忽略不稳定测试会降低测试套件可信度修复根因或隔离处理
截图未屏蔽动态内容内容每次都不同,测试永远失败屏蔽时间戳、头像、图表等动态内容
无可访问性检查缺失关键质量门禁每个页面都要跑axe-core检测

Integration Points

集成关联

SkillRelationship
senior-frontend
Frontend components are tested by E2E tests
testing-strategy
E2E tests are the top of the testing pyramid
acceptance-testing
User flow tests serve as acceptance tests
performance-optimization
Performance budgets can be verified in E2E
code-review
Review checks that tests use accessible locators
security-review
Security headers and auth flows tested in E2E

技能关联关系
senior-frontend
前端组件由E2E测试覆盖
testing-strategy
E2E测试是测试金字塔的顶层
acceptance-testing
用户流程测试可作为验收测试
performance-optimization
性能指标可在E2E测试中验证
code-review
代码评审检查测试是否使用可访问性定位器
security-review
安全头与鉴权流程可在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
    waitForTimeout
    (use proper assertions)
  • 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.
灵活适配 — 根据项目核心路径调整测试深度。强烈建议使用页面对象模型模式与可访问性定位器。所有页面必须强制进行可访问性检查。视觉回归基线必须在合并前完成审核。