Skip to main content

qa-e2e-playwright

Playwright E2E 测试完整方法论,涵盖项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧

Informações da origem

Repositório
echoVic/boss-skill
Última atividade na origem
29 de setembro de 2026 às 03:37
Idioma detectado do SKILL.md
chinês
Estrelas
562
Forks
51

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
qa/e2e-playwright
description
Playwright E2E 测试完整方法论,涵盖项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧
version
1.0.0
agent
qa
type
methodology
user-invocable
false
agent-invocable
true
dependencies
["shared/tech-stack-detection"]
triggers
["需要编写或执行 E2E 测试时","需要配置 Playwright 测试环境时","门禁要求 E2E 测试通过时","需要视觉回归测试时"]
metadata
{"internal":true}
# Playwright E2E 测试方法论 ## 适用场景 - Web 项目需要编写端到端测试 - 门禁(Gate 1)要求 E2E 测试通过 - 需要覆盖关键用户流程的自动化验证 - 需要多浏览器/多视口兼容性验证 - 需要视觉回归测试 --- ## 1. 项目初始化 ### 1.1 安装 ```bash # 新项目初始化(推荐) npm init playwright@latest # 已有项目添加 npm install -D @playwright/test npx playwright install ``` ### 1.2 配置文件(`playwright.config.ts`) ```typescript import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', // 测试产物目录 outputDir: './e2e/test-results', // 全局超时 timeout: 30_000, expect: { timeout: 5_000 }, // 并行执行 fullyParallel: true, workers: process.env.CI ? 1 : undefined, // 失败重试(CI 中重试一次减少 flaky) retries: process.env.CI ? 1 : 0, // 报告 reporter: [ ['html', { outputFolder: './e2e/playwright-report' }], ['json', { outputFile: './e2e/test-results/results.json' }], // CI 中额外输出到 stdout ...(process.env.CI ? [['github'] as const] : []), ], // 全局配置 use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', // 失败时自动截图 screenshot: 'only-on-failure', // 失败时录制 trace trace: 'on-first-retry', // 失败时录制视频 video: 'on-first-retry', }, // 多浏览器 + 移动端视口 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, timeout: 120_000, }, }); ``` **关键配置说明**: | 配置项 | 作用 | 建议值 | |--------|------|--------| | `fullyParallel` | 测试文件间并行执行 | `true` | | `workers` | 并行 worker 数 | CI 为 1,本地默认 | | `retries` | 失败重试次数 | CI 为 1,本地为 0 | | `trace` | 失败时生成可视化时间线 | `on-first-retry` | | `webServer` | 自动启动开发服务器 | 必须配置 | ### 1.3 目录结构 ``` e2e/ ├── playwright.config.ts # 配置文件(或放在项目根目录) ├── fixtures/ # 自定义 fixtures │ ├── base.ts # 扩展 base test │ └── auth.ts # 认证 fixture ├── pages/ # Page Object Models │ ├── login.page.ts │ ├── dashboard.page.ts │ └── components/ # 可复用组件 POM │ ├── navbar.component.ts │ └── modal.component.ts ├── specs/ # 测试用例 │ ├── auth/ │ │ ├── login.spec.ts │ │ └── register.spec.ts │ ├── dashboard/ │ │ └── dashboard.spec.ts │ └── crud/ │ └── user-management.spec.ts ├── helpers/ # 测试工具 │ ├── seed.ts # 数据种子 │ └── cleanup.ts # 数据清理 ├── test-results/ # 测试产物(gitignore) └── playwright-report/ # HTML 报告(gitignore) ``` --- ## 2. Page Object Model(POM) ### 2.1 核心原则 - **每个页面一个 POM 类**:封装定位器和操作方法 - **不暴露 Locator**:外部只调用语义化方法 - **组件级复用**:导航栏、弹窗等提取为独立组件 POM ### 2.2 基础 POM ```typescript // e2e/pages/login.page.ts import { type Page, type Locator } from '@playwright/test'; export class LoginPage { private readonly emailInput: Locator; private readonly passwordInput: Locator; private readonly submitButton: Locator; private readonly errorMessage: Locator; constructor(private readonly page: Page) { this.emailInput = page.getByLabel('邮箱'); this.passwordInput = page.getByLabel('密码'); this.submitButton = page.getByRole('button', { name: '登录' }); this.errorMessage = page.getByRole('alert'); } async goto() { await this.page.goto('/login'); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } async getErrorMessage() { return this.errorMessage.textContent(); } } ``` ### 2.3 组件 POM ```typescript // e2e/pages/components/navbar.component.ts import { type Page, type Locator } from '@playwright/test'; export class NavbarComponent { private readonly userMenu: Locator; private readonly logoutButton: Locator; constructor(private readonly page: Page) { this.userMenu = page.getByTestId('user-menu'); this.logoutButton = page.getByRole('menuitem', { name: '退出登录' }); } async logout() { await this.userMenu.click(); await this.logoutButton.click(); } async getUserDisplayName() { return this.userMenu.textContent(); } } ``` ### 2.4 定位器优先级 选择定位器时遵循以下优先级(可靠性从高到低): | 优先级 | 方法 | 示例 | 说明 | |--------|------|------|------| | 1 | `getByRole` | `getByRole('button', { name: '提交' })` | 无障碍语义,最稳定 | | 2 | `getByLabel` | `getByLabel('邮箱')` | 表单元素首选 | | 3 | `getByPlaceholder` | `getByPlaceholder('请输入邮箱')` | 备选 | | 4 | `getByText` | `getByText('欢迎回来')` | 静态文本 | | 5 | `getByTestId` | `getByTestId('submit-btn')` | 无语义标记时的兜底 | | 6 | CSS/XPath | `page.locator('.btn-primary')` | **尽量避免** | --- ## 3. 认证状态复用 ### 3.1 Global Setup 方式 ```typescript // e2e/global-setup.ts import { chromium, type FullConfig } from '@playwright/test'; async function globalSetup(config: FullConfig) { const browser = await chromium.launch(); const page = await browser.newPage(); // 执行登录 await page.goto('http://localhost:3000/login'); await page.getByLabel('邮箱').fill('admin@example.com'); await page.getByLabel('密码').fill('password'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForURL('/dashboard'); // 保存认证状态 await page.context().storageState({ path: './e2e/.auth/admin.json' }); await browser.close(); } export default globalSetup; ``` **配置引用**: ```typescript // playwright.config.ts export default defineConfig({ globalSetup: './e2e/global-setup.ts', projects: [ // 不带认证的测试 { name: 'public', testMatch: /public\.spec\.ts/ }, // 带认证的测试 { name: 'authenticated', use: { storageState: './e2e/.auth/admin.json' }, testIgnore: /public\.spec\.ts/, }, ], }); ``` ### 3.2 多角色认证 ```typescript // e2e/fixtures/auth.ts import { test as base } from '@playwright/test'; type AuthFixtures = { adminPage: Page; userPage: Page; }; export const test = base.extend<AuthFixtures>({ adminPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: './e2e/.auth/admin.json', }); const page = await context.newPage(); await use(page); await context.close(); }, userPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: './e2e/.auth/user.json', }); const page = await context.newPage(); await use(page); await context.close(); }, }); ``` --- ## 4. API Mocking ### 4.1 使用 `page.route` 拦截请求 ```typescript test('显示用户列表(API Mock)', async ({ page }) => { // 拦截 API 请求 await page.route('/api/users', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: '张三', email: 'zhang@example.com' }, { id: 2, name: '李四', email: 'li@example.com' }, ]), }); }); await page.goto('/users'); await expect(page.getByText('张三')).toBeVisible(); await expect(page.getByText('李四')).toBeVisible(); }); ``` ### 4.2 模拟错误响应 ```typescript test('API 失败时显示错误提示', async ({ page }) => { await page.route('/api/users', async (route) => { await route.fulfill({ status: 500, body: 'Internal Server Error' }); }); await page.goto('/users'); await expect(page.getByText('加载失败')).toBeVisible(); await expect(page.getByRole('button', { name: '重试' })).toBeVisible(); }); ``` ### 4.3 Mock 与真实请求混合 ```typescript test('部分 API Mock', async ({ page }) => { // 只 Mock 第三方支付接口,其余走真实请求 await page.route('**/api/payment/**', async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ success: true, transactionId: 'mock-tx-001' }), }); }); await page.goto('/checkout'); // ... 执行支付流程 }); ``` ### 4.4 何时用 Mock vs 真实 API | 场景 | 建议 | |------|------| | 核心用户路径 | **真实 API**(关键路径证据规则) | | 第三方服务(支付、邮件) | Mock | | 错误/边界状态 | Mock | | 加载状态、空数据 | Mock | | 数据量大的列表/分页 | Mock + 至少一条真实路径 | ⚠️ **Boss 门禁规则**:核心用户路径只由 Mock 证明的,必须标记为**未验证**,不能作为发布证据。 --- ## 5. 关键用户流程测试(必须覆盖) ### 5.1 CRUD 完整流程 ```typescript // e2e/specs/crud/user-management.spec.ts import { test, expect } from '@playwright/test'; import { UserListPage } from '../../pages/user-list.page'; import { UserFormPage } from '../../pages/user-form.page'; test.describe('用户管理 CRUD', () => { let userList: UserListPage; let userForm: UserFormPage; test.beforeEach(async ({ page }) => { userList = new UserListPage(page); userForm = new UserFormPage(page); await userList.goto(); }); test('创建 → 编辑 → 删除完整流程', async ({ page }) => { // 创建 await userList.clickAddUser(); await userForm.fillName('测试用户'); await userForm.fillEmail('test@example.com'); await userForm.submit();
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub