Vue 3 与 Playwright 高级 E2E 测试

概述

Playwright 是一款现代化的端到端(E2E)测试框架,由 Microsoft 开发,支持 Chromium、Firefox 和 WebKit 浏览器。它提供了自动等待、网络拦截、截图对比、多标签页测试等高级功能,与 Vue 3 应用完美兼容。本集将深入探讨 Playwright 的高级特性和最佳实践,帮助你构建全面、可靠的 E2E 测试套件。

核心知识点

1. Playwright 基础配置

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './tests/e2e', // 测试文件目录
  timeout: 30 * 1000, // 单个测试超时时间
  expect: {
    timeout: 5000 // 断言超时时间
  },
  fullyParallel: true, // 并行运行测试
  forbidOnly: !!process.env.CI, // CI 环境禁止单独运行测试
  retries: process.env.CI ? 2 : 0, // CI 环境重试次数
  workers: process.env.CI ? 1 : undefined, // CI 环境工作进程数
  reporter: [['html', { outputFolder: 'playwright-report' }]], // 测试报告格式
  use: {
    baseURL: 'http://localhost:5173', // 测试基础 URL
    trace: 'on-first-retry', // 重试时记录追踪
    screenshot: 'only-on-failure', // 失败时截图
    video: 'retain-on-failure', // 失败时保留视频
    headless: process.env.CI ? true : false // CI 环境无头模式
  },
  // 配置不同浏览器
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] }
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] }
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] }
    }
  ],
  // 开发服务器配置
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI
  }
})

2. 测试用例结构

// tests/e2e/example.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Example Tests', () => {
  test.beforeEach(async ({ page }) => {
    // 每个测试前访问首页
    await page.goto('/')
  })

  test('has title', async ({ page }) => {
    // 断言页面标题
    await expect(page).toHaveTitle(/Vue 3 App/)
  })

  test('get started link', async ({ page }) => {
    // 点击链接
    await page.getByRole('link', { name: 'Get Started' }).click()
    
    // 断言新页面 URL
    await expect(page).toHaveURL(/.*guide/)
  })
})

3. Vue 组件交互

// tests/e2e/counter.spec.ts
import { test, expect } from '@playwright/test'

test('counter increments when button is clicked', async ({ page }) => {
  // 访问计数器页面
  await page.goto('/counter')
  
  // 断言初始值
  await expect(page.getByText('Count: 0')).toBeVisible()
  
  // 点击增加按钮
  await page.getByRole('button', { name: 'Increment' }).click()
  
  // 断言更新后的值
  await expect(page.getByText('Count: 1')).toBeVisible()
  
  // 点击减少按钮
  await page.getByRole('button', { name: 'Decrement' }).click()
  
  // 断言最终值
  await expect(page.getByText('Count: 0')).toBeVisible()
})

4. 表单测试

// tests/e2e/form.spec.ts
import { test, expect } from '@playwright/test'

test('form submission works correctly', async ({ page }) => {
  // 访问表单页面
  await page.goto('/form')
  
  // 填写表单
  await page.getByLabel('Name').fill('John Doe')
  await page.getByLabel('Email').fill('john@example.com')
  await page.getByLabel('Message').fill('Hello Playwright!')
  
  // 提交表单
  await page.getByRole('button', { name: 'Submit' }).click()
  
  // 断言成功消息
  await expect(page.getByText('Form submitted successfully!')).toBeVisible()
  
  // 断言表单重置
  await expect(page.getByLabel('Name')).toBeEmpty()
  await expect(page.getByLabel('Email')).toBeEmpty()
  await expect(page.getByLabel('Message')).toBeEmpty()
})

5. 异步操作测试

// tests/e2e/async.spec.ts
import { test, expect } from '@playwright/test'

test('async data loads correctly', async ({ page }) => {
  // 访问异步数据页面
  await page.goto('/async')
  
  // 断言加载状态
  await expect(page.getByText('Loading...')).toBeVisible()
  
  // 等待数据加载完成
  await expect(page.getByText('Async Data Loaded')).toBeVisible()
  
  // 断言数据内容
  await expect(page.getByRole('list')).toContainText('Item 1')
  await expect(page.getByRole('list')).toContainText('Item 2')
  await expect(page.getByRole('list')).toContainText('Item 3')
})

6. 网络拦截

// tests/e2e/mocked-api.spec.ts
import { test, expect } from '@playwright/test'

test('mocks API response', async ({ page }) => {
  // 拦截 API 请求
  await page.route('**/api/data', async (route) => {
    // 返回模拟数据
    await route.fulfill({
      status: 200,
      body: JSON.stringify({
        data: ['Mocked Item 1', 'Mocked Item 2', 'Mocked Item 3']
      })
    })
  })
  
  // 访问页面
  await page.goto('/async')
  
  // 断言模拟数据
  await expect(page.getByRole('list')).toContainText('Mocked Item 1')
  await expect(page.getByRole('list')).toContainText('Mocked Item 2')
  await expect(page.getByRole('list')).toContainText('Mocked Item 3')
})

7. 多标签页测试

// tests/e2e/multiple-tabs.spec.ts
import { test, expect } from '@playwright/test'

test('multiple tabs navigation', async ({ browser }) => {
  // 创建两个页面
  const context = await browser.newContext()
  const page1 = await context.newPage()
  const page2 = await context.newPage()
  
  // 页面 1 访问首页
  await page1.goto('/')
  await expect(page1).toHaveTitle(/Vue 3 App/)
  
  // 页面 2 访问关于页
  await page2.goto('/about')
  await expect(page2).toHaveURL(/.*about/)
  
  // 关闭页面
  await page1.close()
  await page2.close()
})

8. 文件上传测试

// tests/e2e/file-upload.spec.ts
import { test, expect } from '@playwright/test'
import path from 'path'

test('file upload works correctly', async ({ page }) => {
  // 访问文件上传页面
  await page.goto('/upload')
  
  // 获取文件路径
  const filePath = path.join(__dirname, 'test-file.txt')
  
  // 上传文件
  await page.getByLabel('File').setInputFiles(filePath)
  
  // 提交表单
  await page.getByRole('button', { name: 'Upload' }).click()
  
  // 断言上传成功
  await expect(page.getByText('File uploaded successfully!')).toBeVisible()
  await expect(page.getByText('test-file.txt')).toBeVisible()
})

9. 截图对比测试

// tests/e2e/visual-regression.spec.ts
import { test, expect } from '@playwright/test'

test('visual regression test', async ({ page }) => {
  // 访问页面
  await page.goto('/')
  
  // 匹配完整页面截图
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixelRatio: 0.1 // 允许 10% 的像素差异
  })
  
  // 匹配特定元素截图
  await expect(page.getByRole('header')).toHaveScreenshot('header.png')
})

10. 移动端测试

// playwright.config.ts - 添加移动端配置
projects: [
  // ... 桌面浏览器配置
  {
    name: 'Mobile Chrome',
    use: { ...devices['Pixel 5'] }
  },
  {
    name: 'Mobile Safari',
    use: { ...devices['iPhone 13'] }
  }
]

11. 自定义测试函数

// tests/e2e/utils.ts
import { Page } from '@playwright/test'

export async function login(page: Page, username: string, password: string) {
  await page.goto('/login')
  await page.getByLabel('Username').fill(username)
  await page.getByLabel('Password').fill(password)
  await page.getByRole('button', { name: 'Login' }).click()
}

export async function logout(page: Page) {
  await page.getByRole('button', { name: 'Logout' }).click()
}
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
import { login, logout } from './utils'

test('login and logout flow', async ({ page }) => {
  // 登录
  await login(page, 'testuser', 'password123')
  
  // 断言登录成功
  await expect(page.getByText('Welcome, testuser!')).toBeVisible()
  
  // 登出
  await logout(page)
  
  // 断言登出成功
  await expect(page.getByText('Login')).toBeVisible()
})

最佳实践

1. 测试命名规范

  • 测试文件名:使用 .spec.ts 后缀
  • 测试套件:使用 test.describe 组织相关测试
  • 测试用例:使用清晰、描述性的名称
  • 使用 test 而不是 it 定义测试用例

2. 定位策略

  • 优先使用 getByRole 定位可访问元素
  • 使用 getByText 定位文本内容
  • 使用 getByLabel 定位表单元素
  • 使用 getByTestId 定位带有 data-testid 属性的元素

3. 测试隔离原则

  • 每个测试应该独立运行,不依赖其他测试的状态
  • 使用 test.beforeEachtest.afterEach 管理测试状态
  • 使用 test.beforeAlltest.afterAll 管理测试套件状态
  • 避免共享可变状态

4. 异步测试最佳实践

  • 利用 Playwright 的自动等待特性
  • 避免使用 setTimeout
  • 使用 await expect() 等待元素可见或满足条件
  • 使用 page.waitForResponse() 等待网络请求完成

5. 网络请求处理

  • 使用 page.route() 拦截和模拟 API 请求
  • 使用 page.waitForRequest() 等待特定请求
  • 使用 page.waitForResponse() 等待特定响应
  • 为测试环境配置专门的 API 端点

6. 性能测试

  • 使用 page.tracing.start()page.tracing.stop() 记录性能追踪
  • 测量页面加载时间
  • 监控 API 请求响应时间
  • 分析关键渲染路径

7. CI/CD 集成

  • 在 CI 环境中使用无头模式运行测试
  • 配置测试报告生成
  • 设置适当的超时和重试策略
  • 集成测试结果到代码审查流程

8. 测试数据管理

  • 使用测试数据生成器
  • 为不同测试场景准备不同的测试数据
  • 清理测试产生的数据
  • 考虑使用测试数据库

常见问题和解决方案

1. 测试不稳定问题

问题:测试在本地通过,但在 CI 环境中失败

解决方案

  • 增加测试超时时间
  • 确保使用自动等待,避免硬编码等待
  • 检查 CI 环境的浏览器版本和配置
  • 增加重试次数

2. 元素定位问题

问题:无法定位到元素

解决方案

  • 检查元素的可访问性属性
  • 使用更具体的定位器
  • 确保元素在 DOM 中存在
  • 检查元素是否被其他元素覆盖

3. 网络请求问题

问题:API 请求失败或超时

解决方案

  • 使用 page.route() 模拟 API 响应
  • 检查网络连接和 API 端点
  • 增加请求超时时间
  • 检查 CORS 配置

4. 截图对比失败

问题:视觉回归测试失败,截图不匹配

解决方案

  • 更新基准截图
  • 调整 maxDiffPixelRatio 允许更大的差异
  • 确保测试环境一致
  • 排除动态内容区域

5. 测试速度问题

问题:测试套件运行缓慢

解决方案

  • 并行运行测试
  • 减少单个测试文件的测试数量
  • 优化测试数据生成
  • 避免在测试中使用真实的数据库或网络请求

进阶学习资源

  1. 官方文档

  2. 工具和库

  3. 学习案例

  4. 最佳实践指南

实践练习

练习 1:基础测试套件

  1. 初始化一个 Vue 3 + Playwright 项目
  2. 配置 Playwright 和测试环境
  3. 编写基础的页面导航测试
  4. 测试表单提交功能

练习 2:高级交互测试

  1. 实现一个包含文件上传的组件
  2. 编写文件上传测试
  3. 测试拖拽功能
  4. 测试键盘快捷键

练习 3:网络拦截和模拟

  1. 创建一个依赖外部 API 的组件
  2. 使用 page.route() 模拟不同的 API 响应
  3. 测试组件的各种状态(加载、成功、错误)
  4. 测试 API 错误处理

练习 4:视觉回归测试

  1. 实现一个复杂的 UI 组件
  2. 编写视觉回归测试
  3. 故意修改组件样式,观察测试失败
  4. 更新基准截图,验证测试通过

练习 5:移动端和多浏览器测试

  1. 配置移动端测试
  2. 在不同浏览器中运行测试
  3. 测试响应式设计
  4. 分析测试结果和性能

总结

Playwright 是一款强大的 E2E 测试框架,与 Vue 3 应用完美兼容。通过掌握 Playwright 的高级特性和最佳实践,你可以构建全面、可靠的 E2E 测试套件,确保你的 Vue 3 应用在各种环境和浏览器中正常工作。从基础配置到高级功能,从页面交互到网络拦截,Playwright 提供了完整的 E2E 测试解决方案。

下一集我们将学习 Vue 3 与 Cypress 高级 E2E 测试,敬请期待!

« 上一篇 Vue 3与Vitest高级测试 - 现代化测试全栈解决方案 下一篇 » 122-vue3-cypress-advanced-e2e-testing