Skip to main content

e2e-testing

Playwright E2E 测试模式、页面对象模型、配置、CI/CD 集成、工件管理和不稳定测试策略。

소스 정보

저장소
aaione/everything-claude-code-zh
최근 소스 활동
2026년 5월 31일 16:26
감지된 SKILL.md 언어
중국어
스타
30
포크
14

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
e2e-testing
description
Playwright E2E 测试模式、页面对象模型、配置、CI/CD 集成、工件管理和不稳定测试策略。
# E2E 测试模式 用于构建稳定、快速和可维护的 E2E 测试套件的全面 Playwright 模式。 ## 测试文件组织 ``` tests/ ├── e2e/ │ ├── auth/ │ │ ├── login.spec.ts │ │ ├── logout.spec.ts │ │ └── register.spec.ts │ ├── features/ │ │ ├── browse.spec.ts │ │ ├── search.spec.ts │ │ └── create.spec.ts │ └── api/ │ └── endpoints.spec.ts ├── fixtures/ │ ├── auth.ts │ └── data.ts └── playwright.config.ts ``` ## 页面对象模型 (POM) ```typescript import { Page, Locator } from '@playwright/test' export class ItemsPage { readonly page: Page readonly searchInput: Locator readonly itemCards: Locator readonly createButton: Locator constructor(page: Page) { this.page = page this.searchInput = page.locator('[data-testid="search-input"]') this.itemCards = page.locator('[data-testid="item-card"]') this.createButton = page.locator('[data-testid="create-btn"]') } async goto() { await this.page.goto('/items') await this.page.waitForLoadState('networkidle') } async search(query: string) { await this.searchInput.fill(query) await this.page.waitForResponse(resp => resp.url().includes('/api/search')) await this.page.waitForLoadState('networkidle') } async getItemCount() { return await this.itemCards.count() } } ``` ## 测试结构 ```typescript import { test, expect } from '@playwright/test' import { ItemsPage } from '../../pages/ItemsPage' test.describe('Item Search', () => { let itemsPage: ItemsPage test.beforeEach(async ({ page }) => { itemsPage = new ItemsPage(page) await itemsPage.goto() }) test('should search by keyword', async ({ page }) => { await itemsPage.search('test') const count = await itemsPage.getItemCount() expect(count).toBeGreaterThan(0) await expect(itemsPage.itemCards.first()).toContainText(/test/i) await page.screenshot({ path: 'artifacts/search-results.png' }) }) test('should handle no results', async ({ page }) => { await itemsPage.search('xyznonexistent123') await expect(page.locator('[data-testid="no-results"]')).toBeVisible() expect(await itemsPage.getItemCount()).toBe(0) }) }) ``` ## 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', { outputFolder: 'playwright-report' }], ['junit', { outputFile: 'playwright-results.xml' }], ['json', { outputFile: 'playwright-results.json' }] ], use: { baseURL: process.env.BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', actionTimeout: 10000, navigationTimeout: 30000, }, 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'] } }, ], webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120000, }, }) ``` ## 不稳定测试模式 ### 隔离 ```typescript test('flaky: complex search', async ({ page }) => { test.fixme(true, 'Flaky - Issue #123') // 测试代码... }) test('conditional skip', async ({ page }) => { test.skip(process.env.CI, 'Flaky in CI - Issue #123') // 测试代码... }) ``` ### 识别不稳定性 ```bash npx playwright test tests/search.spec.ts --repeat-each=10 npx playwright test tests/search.spec.ts --retries=3 ``` ### 常见原因和修复 **竞争条件:** ```typescript // 坏:假设元素已准备就绪 await page.click('[data-testid="button"]') // 好:自动等待定位符 await page.locator('[data-testid="button"]').click() ``` **网络计时:** ```typescript // 坏:任意超时 await page.waitForTimeout(5000) // 好:等待特定条件 await page.waitForResponse(resp => resp.url().includes('/api/data')) ``` **动画计时:** ```typescript // 坏:在动画期间点击 await page.click('[data-testid="menu-item"]') // 好:等待稳定性 await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' }) await page.waitForLoadState('networkidle') await page.locator('[data-testid="menu-item"]').click() ``` ## 工件管理 ### 截图 ```typescript await page.screenshot({ path: 'artifacts/after-login.png' }) await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true }) await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' }) ``` ### 追踪 ```typescript await browser.startTracing(page, { path: 'artifacts/trace.json', screenshots: true, snapshots: true, }) // ... 测试操作 ... await browser.stopTracing() ``` ### 视频 ```typescript // 在 playwright.config.ts 中 use: { video: 'retain-on-failure', videosPath: 'artifacts/videos/' } ``` ## CI/CD 集成 ```yaml # .github/workflows/e2e.yml name: E2E Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test env: BASE_URL: ${{ vars.STAGING_URL }} - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 ``` ## 测试报告模板 ```markdown # E2E 测试报告 **日期:** YYYY-MM-DD HH:MM **持续时间:** Xm Ys **状态:** 通过 / 失败 ## 摘要 - 总计:X | 通过:Y (Z%) | 失败:A | 不稳定:B | 跳过:C ## 失败的测试 ### test-name **文件:** `tests/e2e/feature.spec.ts:45` **错误:** 预期元素可见 **截图:** artifacts/failed.png **建议修复:** [描述] ## 工件 - HTML 报告:playwright-report/index.html - 截图:artifacts/*.png - 视频:artifacts/videos/*.webm - 追踪:artifacts/*.zip ``` ## 钱包 / Web3 测试 ```typescript test('wallet connection', async ({ page, context }) => { // 模拟钱包提供程序 await context.addInitScript(() => { window.ethereum = { isMetaMask: true, request: async ({ method }) => { if (method === 'eth_requestAccounts') return ['0x1234567890123456789012345678901234567890'] if (method === 'eth_chainId') return '0x1' } } }) await page.goto('/') await page.locator('[data-testid="connect-wallet"]').click() await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234') }) ``` ## 金融 / 关键流程测试 ```typescript test('trade execution', async ({ page }) => { // 在生产环境跳过 — 真钱 test.skip(process.env.NODE_ENV === 'production', 'Skip on 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') // 验证预览 const preview = page.locator('[data-testid="trade-preview"]') await expect(preview).toContainText('1.0') // 确认并等待区块链 await page.locator('[data-testid="confirm-trade"]').click() await page.waitForResponse( resp => resp.url().includes('/api/trade') && resp.status() === 200, { timeout: 30000 } ) await expect(page.locator('[data-testid="trade-success"]')).toBeVisible() }) ```
GitHub에서 보기