Skip to main content

qa-e2e-playwright

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

Quellinformationen

Repository
echoVic/boss-skill
Letzte Quellaktivität
29. September 2026 um 03:37
Erkannte Sprache von SKILL.md
Chinesisch
Sterne
563
Forks
51

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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();
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen