| | |
| | | --- |
| | | name: e2e-testing |
| | | description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies. |
| | | origin: ECC |
| | | --- |
| | | # Playwright E2E 测试模式参考 |
| | | |
| | | # E2E Testing Patterns |
| | | 构建稳定、快速、可维护的 E2E 测试套件的 Playwright 模式集合。 |
| | | |
| | | Comprehensive Playwright patterns for building stable, fast, and maintainable E2E test suites. |
| | | |
| | | ## Test File Organization |
| | | ## 测试文件组织 |
| | | |
| | | ``` |
| | | tests/ |
| | | test/ |
| | | ├── e2e/ |
| | | │ ├── auth/ |
| | | │ │ ├── login.spec.ts |
| | |
| | | └── playwright.config.ts |
| | | ``` |
| | | |
| | | ## Page Object Model (POM) |
| | | ## 页面对象模型(POM) |
| | | |
| | | ```typescript |
| | | import { Page, Locator } from '@playwright/test' |
| | |
| | | } |
| | | ``` |
| | | |
| | | ## Test Structure |
| | | ## 测试结构 |
| | | |
| | | ```typescript |
| | | import { test, expect } from '@playwright/test' |
| | | import { ItemsPage } from '../../pages/ItemsPage' |
| | | |
| | | test.describe('Item Search', () => { |
| | | test.describe('商品搜索', () => { |
| | | let itemsPage: ItemsPage |
| | | |
| | | test.beforeEach(async ({ page }) => { |
| | |
| | | await itemsPage.goto() |
| | | }) |
| | | |
| | | test('should search by keyword', async ({ page }) => { |
| | | await itemsPage.search('test') |
| | | test('根据关键词搜索', async ({ page }) => { |
| | | await itemsPage.search('测试商品') |
| | | |
| | | const count = await itemsPage.getItemCount() |
| | | expect(count).toBeGreaterThan(0) |
| | | |
| | | await expect(itemsPage.itemCards.first()).toContainText(/test/i) |
| | | await expect(itemsPage.itemCards.first()).toContainText(/测试/i) |
| | | await page.screenshot({ path: 'artifacts/search-results.png' }) |
| | | }) |
| | | |
| | | test('should handle no results', async ({ page }) => { |
| | | test('搜索无结果时显示空状态', async ({ page }) => { |
| | | await itemsPage.search('xyznonexistent123') |
| | | |
| | | await expect(page.locator('[data-testid="no-results"]')).toBeVisible() |
| | |
| | | }) |
| | | ``` |
| | | |
| | | ## Playwright Configuration |
| | | ## Playwright 配置 |
| | | |
| | | ```typescript |
| | | import { defineConfig, devices } from '@playwright/test' |
| | | |
| | | export default defineConfig({ |
| | | testDir: './tests/e2e', |
| | | testDir: './test/e2e', |
| | | fullyParallel: true, |
| | | forbidOnly: !!process.env.CI, |
| | | retries: process.env.CI ? 2 : 0, |
| | |
| | | ['json', { outputFile: 'playwright-results.json' }] |
| | | ], |
| | | use: { |
| | | baseURL: process.env.BASE_URL || 'http://localhost:3000', |
| | | baseURL: process.env.BASE_URL || 'http://localhost:5173', |
| | | trace: 'on-first-retry', |
| | | screenshot: 'only-on-failure', |
| | | video: 'retain-on-failure', |
| | |
| | | { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, |
| | | ], |
| | | webServer: { |
| | | command: 'npm run dev', |
| | | url: 'http://localhost:3000', |
| | | command: 'pnpm dev', |
| | | url: 'http://localhost:5173', |
| | | reuseExistingServer: !process.env.CI, |
| | | timeout: 120000, |
| | | }, |
| | | }) |
| | | ``` |
| | | |
| | | ## Flaky Test Patterns |
| | | ## 不稳定测试模式 |
| | | |
| | | ### Quarantine |
| | | ### 隔离 |
| | | |
| | | ```typescript |
| | | test('flaky: complex search', async ({ page }) => { |
| | | test.fixme(true, 'Flaky - Issue #123') |
| | | // test code... |
| | | test('不稳定测试:复杂搜索', async ({ page }) => { |
| | | test.fixme(true, '不稳定 - Issue #123') |
| | | // 测试代码... |
| | | }) |
| | | |
| | | test('conditional skip', async ({ page }) => { |
| | | test.skip(process.env.CI, 'Flaky in CI - Issue #123') |
| | | // test code... |
| | | test('条件跳过', async ({ page }) => { |
| | | test.skip(process.env.CI, 'CI 环境不稳定 - Issue #123') |
| | | // 测试代码... |
| | | }) |
| | | ``` |
| | | |
| | | ### Identify Flakiness |
| | | ### 识别不稳定性 |
| | | |
| | | ```bash |
| | | npx playwright test tests/search.spec.ts --repeat-each=10 |
| | | npx playwright test tests/search.spec.ts --retries=3 |
| | | npx playwright test test/search.spec.ts --repeat-each=10 |
| | | npx playwright test test/search.spec.ts --retries=3 |
| | | ``` |
| | | |
| | | ### Common Causes & Fixes |
| | | ### 常见原因及修复 |
| | | |
| | | **Race conditions:** |
| | | **竞态条件:** |
| | | ```typescript |
| | | // Bad: assumes element is ready |
| | | // 错误:假设元素已就绪 |
| | | await page.click('[data-testid="button"]') |
| | | |
| | | // Good: auto-wait locator |
| | | // 正确:使用自动等待的定位器 |
| | | await page.locator('[data-testid="button"]').click() |
| | | ``` |
| | | |
| | | **Network timing:** |
| | | **网络时序:** |
| | | ```typescript |
| | | // Bad: arbitrary timeout |
| | | // 错误:任意等待 |
| | | await page.waitForTimeout(5000) |
| | | |
| | | // Good: wait for specific condition |
| | | // 正确:等待特定条件 |
| | | await page.waitForResponse(resp => resp.url().includes('/api/data')) |
| | | ``` |
| | | |
| | | **Animation timing:** |
| | | **动画时序:** |
| | | ```typescript |
| | | // Bad: click during animation |
| | | // 错误:动画过程中点击 |
| | | await page.click('[data-testid="menu-item"]') |
| | | |
| | | // Good: wait for stability |
| | | // 正确:等待稳定后再点击 |
| | | await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' }) |
| | | await page.waitForLoadState('networkidle') |
| | | await page.locator('[data-testid="menu-item"]').click() |
| | | ``` |
| | | |
| | | ## Artifact Management |
| | | ## 产物管理 |
| | | |
| | | ### Screenshots |
| | | ### 截图 |
| | | |
| | | ```typescript |
| | | await page.screenshot({ path: 'artifacts/after-login.png' }) |
| | |
| | | await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' }) |
| | | ``` |
| | | |
| | | ### Traces |
| | | ### 追踪 |
| | | |
| | | ```typescript |
| | | await browser.startTracing(page, { |
| | |
| | | screenshots: true, |
| | | snapshots: true, |
| | | }) |
| | | // ... test actions ... |
| | | // ... 测试操作 ... |
| | | await browser.stopTracing() |
| | | ``` |
| | | |
| | | ### Video |
| | | ### 视频 |
| | | |
| | | ```typescript |
| | | // In playwright.config.ts |
| | | // 在 playwright.config.ts 中配置 |
| | | use: { |
| | | video: 'retain-on-failure', |
| | | videosPath: 'artifacts/videos/' |
| | | } |
| | | ``` |
| | | |
| | | ## CI/CD Integration |
| | | ## CI/CD 集成 |
| | | |
| | | ```yaml |
| | | # .github/workflows/e2e.yml |
| | |
| | | - uses: actions/setup-node@v4 |
| | | with: |
| | | node-version: 20 |
| | | - run: npm ci |
| | | - run: pnpm ci |
| | | - run: npx playwright install --with-deps |
| | | - run: npx playwright test |
| | | env: |
| | |
| | | retention-days: 30 |
| | | ``` |
| | | |
| | | ## Test Report Template |
| | | ## 测试报告模板 |
| | | |
| | | ```markdown |
| | | # E2E Test Report |
| | | # E2E 测试报告 |
| | | |
| | | **Date:** YYYY-MM-DD HH:MM |
| | | **Duration:** Xm Ys |
| | | **Status:** PASSING / FAILING |
| | | **日期:** YYYY-MM-DD HH:MM |
| | | **耗时:** X分Y秒 |
| | | **状态:** 通过 / 失败 |
| | | |
| | | ## Summary |
| | | - Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C |
| | | ## 概要 |
| | | - 总计: X | 通过: Y (Z%) | 失败: A | 不稳定: B | 跳过: C |
| | | |
| | | ## Failed Tests |
| | | ## 失败测试 |
| | | |
| | | ### test-name |
| | | **File:** `tests/e2e/feature.spec.ts:45` |
| | | **Error:** Expected element to be visible |
| | | **Screenshot:** artifacts/failed.png |
| | | **Recommended Fix:** [description] |
| | | ### 测试名称 |
| | | **文件:** `test/e2e/feature.spec.ts:45` |
| | | **错误:** 预期元素可见 |
| | | **截图:** artifacts/failed.png |
| | | **推荐修复:** [描述] |
| | | |
| | | ## Artifacts |
| | | - HTML Report: playwright-report/index.html |
| | | - Screenshots: artifacts/*.png |
| | | - Videos: artifacts/videos/*.webm |
| | | - Traces: artifacts/*.zip |
| | | ## 产物 |
| | | - HTML 报告: playwright-report/index.html |
| | | - 截图: artifacts/*.png |
| | | - 视频: artifacts/videos/*.webm |
| | | - 追踪: artifacts/*.zip |
| | | ``` |
| | | |
| | | ## Wallet / Web3 Testing |
| | | ## Web3/钱包测试 |
| | | |
| | | ```typescript |
| | | test('wallet connection', async ({ page, context }) => { |
| | | // Mock wallet provider |
| | | test('钱包连接', async ({ page, context }) => { |
| | | // Mock 钱包 provider |
| | | await context.addInitScript(() => { |
| | | window.ethereum = { |
| | | isMetaMask: true, |
| | |
| | | }) |
| | | ``` |
| | | |
| | | ## Financial / Critical Flow Testing |
| | | ## 金融/关键流程测试 |
| | | |
| | | ```typescript |
| | | test('trade execution', async ({ page }) => { |
| | | // Skip on production — real money |
| | | test.skip(process.env.NODE_ENV === 'production', 'Skip on production') |
| | | test('交易执行', async ({ page }) => { |
| | | // 生产环境跳过 — 真实资金 |
| | | test.skip(process.env.NODE_ENV === 'production', '生产环境跳过') |
| | | |
| | | await page.goto('/markets/test-market') |
| | | await page.locator('[data-testid="position-yes"]').click() |
| | | await page.locator('[data-testid="trade-amount"]').fill('1.0') |
| | | |
| | | // Verify preview |
| | | // 验证预览 |
| | | const preview = page.locator('[data-testid="trade-preview"]') |
| | | await expect(preview).toContainText('1.0') |
| | | |
| | | // Confirm and wait for blockchain |
| | | // 确认并等待区块链响应 |
| | | await page.locator('[data-testid="confirm-trade"]').click() |
| | | await page.waitForResponse( |
| | | resp => resp.url().includes('/api/trade') && resp.status() === 200, |