2026年4月1日 7 分钟阅读

Playwright:现代 Web 应用的端到端测试框架

tinyash 0 条评论

任何浏览器 · 任何平台 · 一个 API


🦢 什么是 Playwright?

Playwright 是 Microsoft 开源的端到端(E2E)测试框架,专为现代 Web 应用设计。它提供了可靠的自动化测试能力,支持所有主流浏览器和平台,无需妥协。

核心特点

特点说明
跨浏览器支持 Chromium、WebKit、Firefox 所有现代渲染引擎
跨平台Windows、Linux、macOS 本地运行,支持 CI/CD
跨语言TypeScript、JavaScript、Python、.NET、Java
移动模拟原生模拟 Chrome for Android 和 Mobile Safari
无闪测自动等待 + Web 优先断言,消除不稳定测试
完整隔离每个测试独立浏览器上下文,零开销隔离

🎯 为什么选择 Playwright?

传统测试框架的痛点

❌ 需要手动等待元素加载
❌ 测试之间相互污染
❌ 不同浏览器行为不一致
❌ 调试困难,失败后难以复现
❌ 无法模拟真实用户交互

Playwright 的解决方案

✅ 自动等待元素可操作后再执行
✅ 每个测试独立浏览器上下文(类似新浏览器配置文件)
✅ 统一 API,所有浏览器行为一致
✅ 内置 Trace Viewer、录屏、截图、DOM 快照
✅ 使用真实浏览器输入管道,事件完全可信

🚀 快速开始

系统要求

  • Node.js: 20.x / 22.x / 24.x(最新版)
  • Windows: Windows 11+ / Windows Server 2019+ / WSL
  • macOS: macOS 14 (Ventura) 或更高
  • Linux: Debian 12/13, Ubuntu 22.04/24.04 (x86-64 或 arm64)

安装 Playwright

# 使用 npm 初始化新项目
npm init playwright@latest

# 或使用 yarn
yarn create playwright

# 或使用 pnpm
pnpm create playwright

安装过程中的选择

1. 选择语言:TypeScript 或 JavaScript(默认 TypeScript)
2. 测试文件夹名称:tests 或 e2e(默认 tests)
3. 是否添加 GitHub Actions 工作流(推荐用于 CI)
4. 是否安装 Playwright 浏览器(默认 yes)

项目结构

安装完成后,项目结构如下:

my-project/
├── playwright.config.ts    # 测试配置
├── package.json
├── package-lock.json       # 或 yarn.lock / pnpm-lock.yaml
└── tests/
    └── example.spec.ts     # 最小示例测试

运行测试

# 运行所有测试(默认 headless 模式,并行执行)
npx playwright test

# 在浏览器窗口中运行(有头模式)
npx playwright test --headed

# 只运行 Chromium 浏览器
npx playwright test --project=chromium

# 运行单个测试文件
npx playwright test tests/example.spec.ts

# 打开 UI 模式(推荐用于调试)
npx playwright test --ui

查看测试报告

# 测试完成后自动打开 HTML 报告(仅当有失败时)
# 或手动打开
npx playwright show-report

📝 第一个测试

基本示例

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

test('首页应该显示正确的标题', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 等待并检查页面标题
  await expect(page).toHaveTitle(/Example Domain/);
  
  // 检查页面内容
  await expect(page.locator('h1')).toContainText('Example Domain');
});

多浏览器测试

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

test.describe('跨浏览器测试', () => {
  test('应该在所有浏览器中正常工作', async ({ page }) => {
    await page.goto('https://playwright.dev');
    
    // Playwright 会自动等待元素可操作
    await page.click('text=Get Started');
    
    // 断言会自动重试直到条件满足
    await expect(page.locator('h1')).toBeVisible();
  });
});

移动设备模拟

import { test, devices } from '@playwright/test';

test('在 iPhone 上测试', async ({ browser }) => {
  const context = await browser.newContext({
    ...devices['iPhone 13']
  });
  const page = await context.newPage();
  
  await page.goto('https://example.com');
  // 测试移动端布局...
  
  await context.close();
});

🔧 核心 API

浏览器类型

import { chromium, firefox, webkit } from 'playwright';

// 启动 Chromium
const browser = await chromium.launch();

// 启动 Firefox
const browser = await firefox.launch();

// 启动 WebKit (Safari 引擎)
const browser = await webkit.launch();

页面操作

// 导航
await page.goto('https://example.com');
await page.goBack();
await page.goForward();
await page.reload();

// 点击
await page.click('button');
await page.dblclick('button');
await page.hover('button');

// 输入
await page.fill('input[name="email"]', 'test@example.com');
await page.type('input[name="password"]', 'secret');
await page.press('input[name="password"]', 'Enter');

// 选择
await page.selectOption('select#country', 'US');
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');

定位器(Locators)

Playwright 推荐使用用户友好的定位器:

// 按文本
page.getByText('Welcome');
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email');
page.getByPlaceholder('Enter your name');
page.getByAltText('Logo');
page.getByTitle('Help');

// 按测试 ID(推荐)
page.getByTestId('submit-button');

// CSS 选择器
page.locator('.container > .item');

// XPath
page.locator('//button[text()="Submit"]');

断言(Assertions)

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

// 可见性
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached();

// 文本内容
await expect(locator).toContainText('partial');
await expect(locator).toHaveText('exact match');

// 属性
await expect(locator).toHaveAttribute('type', 'submit');
await expect(locator).toHaveClass('active');
await expect(locator).toHaveValue('input value');

// 计数
await expect(locator).toHaveCount(3);

// URL
await expect(page).toHaveURL(/.*dashboard/);

// 截图对比
await expect(page).toHaveScreenshot();

🛠️ 强大工具集

1. Codegen — 录制测试

通过记录操作自动生成测试代码:

# 打开浏览器开始录制
npx playwright codegen

# 指定目标 URL
npx playwright codegen https://example.com

# 指定输出语言
npx playwright codegen --lang=python https://example.com

支持语言:TypeScript、JavaScript、Python、Java、C#


2. Playwright Inspector — 调试器

逐步执行测试,查看点击位置,探索执行日志:

# 调试模式运行
npx playwright test --debug

# 或设置环境变量
PWDEBUG=1 npx playwright test

功能

  • 逐步执行测试
  • 查看实时 DOM 快照
  • 生成选择器
  • 检查点击位置
  • 查看执行日志

3. Trace Viewer — 追踪查看器

捕获测试失败的完整信息:

# 在配置中启用追踪
// playwright.config.ts
export default {
  use: {
    trace: 'on-first-retry'  // 首次重试时记录追踪
  }
};

# 查看追踪文件
npx playwright show-trace trace.zip

追踪内容包括

  • 测试执行录屏
  • 实时 DOM 快照
  • 操作探索器
  • 测试源码
  • 网络请求
  • 控制台日志
  • 错误堆栈

4. UI Mode — UI 测试模式

实时观察测试执行,支持时间旅行调试:

npx playwright test --ui

功能

  • 监视模式(文件变更自动重跑)
  • 实时步骤视图
  • 时间旅行调试
  • 追踪集成
  • 按状态/浏览器过滤

📦 高级功能

浏览器上下文(Browser Contexts)

每个测试获得完全隔离的浏览器上下文:

import { test } from '@playwright/test';

test('测试隔离', async ({ browser }) => {
  // 创建独立上下文(类似新的浏览器配置文件)
  const context1 = await browser.newContext();
  const context2 = await browser.newContext();
  
  const page1 = await context1.newPage();
  const page2 = await context2.newPage();
  
  // 两个页面完全隔离:不同的 Cookie、LocalStorage 等
  await page1.goto('https://example.com');
  await page2.goto('https://example.com');
  
  await context1.close();
  await context2.close();
});

优势

  • 完全测试隔离
  • 零开销(创建仅需几毫秒)
  • 无需清理状态

登录一次,多次使用

保存认证状态并在所有测试中重用:

// playwright.config.ts
export default {
  use: {
    storageState: 'auth.json'  // 使用保存的认证状态
  }
};

// 保存认证状态
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password');
  await page.click('button[type="submit"]');
  await page.waitForURL('https://example.com/dashboard');
  
  // 保存认证状态
  await page.context().storageState({ path: 'auth.json' });
});

多标签、多用户测试

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

test('多用户协作测试', async ({ browser }) => {
  // 创建两个独立用户上下文
  const user1Context = await browser.newContext();
  const user2Context = await browser.newContext();
  
  const user1Page = await user1Context.newPage();
  const user2Page = await user2Context.newPage();
  
  // 用户 1 创建文档
  await user1Page.goto('https://example.com/docs/new');
  await user1Page.fill('input[name="title"]', 'My Doc');
  await user1Page.click('button:has-text("Create")');
  
  // 用户 2 查看并编辑
  await user2Page.goto('https://example.com/docs');
  await user2Page.click('text=My Doc');
  await user2Page.fill('textarea', 'Adding content...');
  
  // 验证两个用户看到相同内容
  await expect(user1Page.locator('textarea')).toHaveValue('Adding content...');
  
  await user1Context.close();
  await user2Context.close();
});

API 测试

Playwright 也支持纯 API 测试:

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

test('API 测试', async ({ request }) => {
  // 发送 POST 请求创建资源
  const createResponse = await request.post('/api/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com'
    }
  });
  
  expect(createResponse.ok()).toBeTruthy();
  const user = await createResponse.json();
  
  // 验证资源已创建
  const getResponse = await request.get(`/api/users/${user.id}`);
  const userData = await getResponse.json();
  
  expect(userData.name).toBe('John Doe');
});

网络拦截与 Mock

import { test } from '@playwright/test';

test('网络拦截', async ({ page }) => {
  // Mock API 响应
  await page.route('**/api/users', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{ id: 1, name: 'Mocked User' }])
    });
  });
  
  // Mock 失败场景
  await page.route('**/api/error', route => {
    route.fulfill({
      status: 500,
      body: 'Internal Server Error'
    });
  });
  
  await page.goto('https://example.com');
});

文件上传下载

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

test('文件上传', async ({ page }) => {
  await page.goto('https://example.com/upload');
  
  const fileInput = page.locator('input[type="file"]');
  await fileInput.setInputFiles(path.join(__dirname, 'test-file.pdf'));
  
  await page.click('button:has-text("Upload")');
  await expect(page.locator('.success')).toBeVisible();
});

test('文件下载', async ({ page }) => {
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    page.click('a:has-text("Download")')
  ]);
  
  // 保存下载的文件
  await download.saveAs('./downloads/my-file.pdf');
  
  // 或获取下载路径
  const downloadPath = download.path();
});

📊 测试配置

完整配置示例

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

export default defineConfig({
  // 测试文件位置
  testDir: './tests',
  
  // 超时设置
  timeout: 30 * 1000,
  expect: {
    timeout: 5000
  },
  
  // 失败重试
  retries: 2,
  
  // 并行执行
  workers: 4,
  
  // 报告器
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results.json' }],
    ['junit', { outputFile: 'junit.xml' }]
  ],
  
  // 共享配置
  use: {
    // 基础 URL
    baseURL: 'https://example.com',
    
    // 追踪设置
    trace: 'on-first-retry',
    
    // 录屏
    video: 'retain-on-failure',
    
    // 截图
    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 12'] }
    }
  ]
});

🔄 CI/CD 集成

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install Playwright browsers
      run: npx playwright install --with-deps
    
    - name: Run Playwright tests
      run: npx playwright test
    
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Docker 运行

FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

CMD ["npx", "playwright", "test"]

🎯 最佳实践

1. 使用用户友好的定位器

// ✅ 推荐
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email');
page.getByTestId('submit-btn');

// ❌ 避免
page.locator('div > div:nth-child(2) > button.blue');
page.locator('//button[@class="btn btn-primary"]');

2. 避免硬编码等待

// ❌ 避免
await page.waitForTimeout(5000);

// ✅ 推荐
await page.waitForSelector('.loaded');
await expect(page.locator('h1')).toBeVisible();

3. 使用 Page Object 模式

// tests/pages/login.page.ts
export class LoginPage {
  constructor(private page: Page) {}
  
  async goto() {
    await this.page.goto('/login');
  }
  
  async login(email: string, password: string) {
    await this.page.fill('input[name="email"]', email);
    await this.page.fill('input[name="password"]', password);
    await this.page.click('button[type="submit"]');
  }
  
  async isLoggedIn() {
    return this.page.url().includes('/dashboard');
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';

test('登录成功', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password');
  await expect(loginPage.isLoggedIn()).toBeTruthy();
});

4. 使用 Fixtures 共享设置

// tests/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';

export const test = base.extend<{
  authenticatedPage: Page;
}>({
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    
    // 登录
    await page.goto('/login');
    await page.fill('input[name="email"]', 'user@example.com');
    await page.fill('input[name="password"]', 'password');
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');
    
    await use(page);
    await context.close();
  }
});

// tests/dashboard.spec.ts
import { test, expect } from './fixtures/auth.fixture';

test('仪表盘应该显示欢迎信息', async ({ authenticatedPage }) => {
  await expect(authenticatedPage.locator('h1'))
    .toContainText('Welcome');
});

📚 学习资源

资源链接
官方文档https://playwright.dev
Python 文档https://playwright.dev/python
.NET 文档https://playwright.dev/dotnet
Java 文档https://playwright.dev/java
GitHub 仓库https://github.com/microsoft/playwright
Discord 社区https://aka.ms/playwright/discord
Stack Overflowhttps://stackoverflow.com/questions/tagged/playwright

🦢 总结

Playwright 是目前最强大的 Web 自动化测试工具之一,它解决了传统测试框架的诸多痛点:

核心优势

优势说明
可靠性自动等待 + 智能断言,消除闪测
速度并行执行 + 轻量上下文,快速反馈
调试Trace Viewer + UI Mode + Inspector,问题一目了然
覆盖所有主流浏览器 + 移动设备模拟
生态多语言支持 + 活跃社区 + 持续更新

适用场景

  • ✅ Web 应用端到端测试
  • ✅ 跨浏览器兼容性测试
  • ✅ 移动端 Web 测试
  • ✅ API 测试
  • ✅ 视觉回归测试
  • ✅ 性能监控
  • ✅ 自动化脚本

不适合

  • ❌ 桌面应用测试(考虑 PyAutoGUI)
  • ❌ 原生移动应用测试(考虑 Appium)
  • ❌ 游戏测试(考虑专用游戏测试框架)

可靠的测试不是奢侈品,而是现代 Web 开发的必需品。

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。