正在加载,请稍候…

Playwright 端到端测试:页面对象模型、视觉测试与 CI 集成

学习从架构到 CI/CD 的 Playwright E2E 测试,涵盖页面对象模型、网络拦截、视觉回归测试和多浏览器自动化。

Playwright End-to-End Testing: Page Object Model, Visual Testing, and CI Integra

2026 年的 Playwright:默认的 E2E 框架

Playwright 已成为 JavaScript 生态系统中端到端测试的标准。关键优势:真正的多浏览器支持(Chromium、Firefox、WebKit)、消除大部分不稳定性的自动等待机制、一流的 TypeScript 支持,以及紧密模拟真实用户与浏览器交互的测试模型。

Playwright End-to-End Testing: Page Object Model, Visual Testing, and CI Integra illustration

安装与配置

npm init playwright@latest
# 交互式:选择 TypeScript、浏览器选择、CI 配置
// playwright.config.ts
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 ? 2 : undefined,

  reporter: [
    ['html'],
    ['github'],
    ['json', { outputFile: 'results.json' }],
  ],

  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:5173',
    trace: 'on-first-retry',
    screenshot: 'only-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:5173',
    reuseExistingServer: !process.env.CI,
  },
})

页面对象模型(Page Object Model)

页面对象模型(POM)是维护可维护 E2E 测试最重要的模式。应用的每个页面都有一个类,封装了选择器和操作。当 UI 发生变化时,你只需更新页面对象,而不是 20 个测试。

// tests/e2e/pages/LoginPage.ts
import { Page, expect } from '@playwright/test'

export class LoginPage {
  private readonly emailInput    = this.page.getByLabel('Email')
  private readonly passwordInput = this.page.getByLabel('Password')
  private readonly submitButton  = this.page.getByRole('button', { name: 'Sign in' })
  private readonly errorMessage  = this.page.getByRole('alert')

  constructor(private readonly page: Page) {}

  async goto() {
    await this.page.goto('/login')
    await expect(this.submitButton).toBeVisible()
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email)
    await this.passwordInput.fill(password)
    await this.submitButton.click()
  }

  async expectErrorMessage(message: string) {
    await expect(this.errorMessage).toBeVisible()
    await expect(this.errorMessage).toContainText(message)
  }
}
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage'

test.describe('Authentication', () => {
  let loginPage: LoginPage

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page)
    await loginPage.goto()
  })

  test('logs in with valid credentials', async ({ page }) => {
    await loginPage.login('user@example.com', 'correct-password')
    await expect(page).toHaveURL('/dashboard')
  })

  test('shows error for invalid password', async () => {
    await loginPage.login('user@example.com', 'wrong-password')
    await loginPage.expectErrorMessage('Invalid email or password')
  })

  test('redirects to originally requested page after login', async ({ page }) => {
    await page.goto('/settings/profile')
    await loginPage.login('user@example.com', 'correct-password')
    await expect(page).toHaveURL('/settings/profile')
  })
})

Playwright End-to-End Testing: Page Object Model, Visual Testing, and CI Integra illustration

网络拦截

import { test, expect } from '@playwright/test'

test.describe('API error handling', () => {
  test('shows error state when API returns 500', async ({ page }) => {
    await page.route('**/api/dashboard/stats', route => {
      route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Internal server error' }),
      })
    })

    await page.goto('/dashboard')
    await expect(page.getByRole('alert')).toContainText('Failed to load statistics')
  })

  test('handles network abort gracefully', async ({ page }) => {
    await page.route('**/api/users', route => route.abort('timedout'))

    await page.goto('/users')
    await expect(page.getByText('Connection error. Please retry.')).toBeVisible()
  })

  test('intercepts and modifies response data', async ({ page }) => {
    await page.route('**/api/user/profile', async route => {
      const response = await route.fetch()
      const body = await response.json()
      body.subscription = 'enterprise'
      body.trialExpired = true
      route.fulfill({ response, body: JSON.stringify(body) })
    })

    await page.goto('/settings')
    await expect(page.getByText('Enterprise Plan')).toBeVisible()
  })
})

视觉回归测试

import { test, expect } from '@playwright/test'

test.describe('Visual regression', () => {
  test('homepage matches snapshot', async ({ page }) => {
    await page.goto('/')
    await expect(page).toHaveScreenshot('homepage.png', {
      fullPage: true,
      maxDiffPixels: 100,
    })
  })

  test('button states match snapshots', async ({ page }) => {
    await page.goto('/ui-components')
    const btn = page.getByTestId('primary-button')

    await expect(btn).toHaveScreenshot('button-default.png')
    await btn.hover()
    await expect(btn).toHaveScreenshot('button-hover.png')
  })
})

设计变更后更新快照:npx playwright test --update-snapshots

Playwright End-to-End Testing: Page Object Model, Visual Testing, and CI Integra illustration

认证状态共享

在每个需要认证的测试前重新登录很慢。Playwright 的存储状态功能可以保存会话:

// tests/e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test'

const authFile = 'playwright/.auth/user.json'

setup('authenticate', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!)
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()
  await expect(page).toHaveURL('/dashboard')
  await page.context().storageState({ path: authFile })
})
// playwright.config.ts — 使用保存的认证状态
projects: [
  { name: 'setup', testMatch: '**/auth.setup.ts' },
  {
    name: 'authenticated-tests',
    use: { storageState: 'playwright/.auth/user.json' },
    dependencies: ['setup'],
  },
],

CI/CD 集成

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main, develop]

jobs:
  test:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

调试失败的测试

# 有头模式——查看浏览器
npx playwright test --headed

# 带 DevTools 的交互式调试器
npx playwright test --debug

# 打开 HTML 报告
npx playwright show-report

# 查看 CI 失败的跟踪
npx playwright show-trace trace.zip

跟踪查看器显示完整的时间线:每个操作、截图、网络请求和控制台消息。调试仅 CI 失败的测试变得非常快。

大型测试套件的性能提示

分片(Sharding):对于非常大的套件,跨多个 CI 机器拆分:

npx playwright test --shard=1/3  # CI Job 1
npx playwright test --shard=2/3  # CI Job 2
npx playwright test --shard=3/3  # CI Job 3

选择性执行:开发期间只运行匹配模式的测试:

npx playwright test --grep "authentication"

Playwright 在可靠性、速度和工具方面的结合使其成为任何认真对待 E2E 测试的项目的正确选择。