playwright 最佳实践
Playwright 是由微软开发的现代化 E2E 自动化测试框架。
🎬 一、Playwright 简介
1. Playwright 是什么?
Playwright 是由微软开发的现代化 E2E 自动化测试框架,支持多浏览器(Chromium、Firefox、WebKit)和多语言(Node.js、Python、Java、C#),特别适合做 UI 自动化测试和爬虫。
示例代码
import { test, expect } from '@playwright/test';
test('使用 Google 搜索', async ({ page }) => {
// 进入 google 搜索页
await page.goto('https://www.google.com');
// 模拟手动输入 Playwright
await page.fill('input[name="q"]', 'Playwright');
// 模拟回车确认搜索
await page.keyboard.press('Enter');
// 校验
await expect(page.locator('#search')).toContainText('Playwright');
});
2. Playwright 能做什么?
- 端到端自动化测试
- 登录、下单、支付、弹窗、表单交互等全流程模拟
- 浏览器爬虫
- 提取页面信息,模拟滚动、点击、登录等
- 回归测试
- CI/CD 中检测新版本是否破坏 UI 功能
- 可视化回归测试
- 截图、视频、trace,查看测试中实际发生了什么
3. Playwright 特性
特性 | 说明 |
---|---|
✅ 多浏览器支持 | 支持 Chromium (Chrome/Edge)、Firefox 和 WebKit (Safari) |
✅ 多语言支持 | 支持 JavaScript/TypeScript、Python、Java、C#/.NET |
✅ 自动等待机制 | 自动等待元素出现、可点击、导航完成等,不容易出 flaky(随机失败)测试 |
✅ 原生支持 iframe、多页签 | 轻松处理复杂页面中的 iframe、弹窗、多标签页 |
✅ 内置测试运行器 | 自带功能完整的 test runner(类似 Jest + Mocha + Chai + Puppeteer) |
✅ 网络拦截与 Mock | 可模拟后端接口,进行无网络依赖测试 |
✅ 跨平台运行 | 支持 Windows、macOS、Linux、本地/CI 环境运行 |
✅ 丰富调试工具 | 提供 UI trace viewer、codegen 工具、screenshot/video trace |
🚀 二、Playwright 实践
1. Playwright 安装依赖
tests/
├── login.spec.ts
├── dashboard.spec.ts
└── utils/
└── auth.ts
playwright.config.ts
$ npm init playwright@latest
# 或手动安装
$ npm install -D @playwright/test
$ npx playwright install
2. 测试用例基础结构
import { test, expect } from '@playwright/test';
// test.describe 理解为测试分组
test.describe('用户模块', () => {
// test 理解为定义了一个测试用例
test('登录', () => {
// 登录相关测试
});
test('注册', () => {
// 注册相关测试
});
});
3. 常见 API 用法
✅ 页面导航 & 交互
await page.goto('https://example.com'); // 打开页面
await page.click('text=Sign in'); // 点击按钮或链接
await page.fill('#username', 'myname'); // 填写输入框
await page.press('#input', 'Enter'); // 模拟按键
await page.selectOption('select#role', 'admin'); // 选择下拉框选项
✅ 元素等待
await page.waitForSelector('#submit-btn'); // 等待出现并可见
await page.waitForSelector('.modal', { state: 'hidden' }); // 等待消失
await expect(page.locator('h1')).toHaveText('Hello'); // 推荐写法
✅ 截图 / 视频 / Trace
await page.screenshot({ path: 'screenshot.png' }); // 截图
await page.context().tracing.start({ screenshots: true, snapshots: true });
await page.context().tracing.stop({ path: 'trace.zip' }); // 查看 UI 行为
✅ 请求拦截 / mock 接口
await page.route('**/api/**', route =>
route.fulfill({ status: 200, body: JSON.stringify({ data: [] }) })
);
4. 模块封装
封装一个登录模块
// login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async login(username: string, password: string) {
await this.page.fill('#user', username);
await this.page.fill('#pass', password);
await this.page.click('text=Login');
}
}
5. 配置项
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Emulates `'prefers-colors-scheme'` media feature.
colorScheme: 'dark',
// Context geolocation.
geolocation: { longitude: 12.492507, latitude: 41.889938 },
// Emulates the user locale.
locale: 'en-GB',
// Grants specified permissions to the browser context.
permissions: ['geolocation'],
// Emulates the user timezone.
timezoneId: 'Europe/Paris',
// Viewport used for all pages in the context.
viewport: { width: 1280, height: 720 },
},
});
5. demo 演示
- ✅ 示例 1:模拟登录
import { test, expect } from '@playwright/test';
test.describe('登录功能', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://yourapp.com/login');
});
test('登录成功跳转到首页', async ({ page }) => {
await page.fill('#username', 'robbie');
await page.fill('#password', 'correct_password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('text=欢迎回来')).toBeVisible();
});
test('登录失败提示错误', async ({ page }) => {
await page.fill('#username', 'robbie');
await page.fill('#password', 'wrong_password');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toHaveText('用户名或密码错误');
});
});
- ✅ 示例 2:等待 loading 消失再点击按钮
test('加载后点击按钮', async ({ page }) => {
await page.goto('https://yourapp.com/profile');
// 等待 loading 动画结束
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
// 再点击保存按钮
await page.click('button.save');
await expect(page.locator('.toast')).toHaveText('保存成功');
});
- ✅ 示例 3:表单验证(空值、错误格式)
test('表单校验错误提示', async ({ page }) => {
await page.goto('https://yourapp.com/register');
await page.click('button[type="submit"]');
await expect(page.locator('#email-error')).toHaveText('邮箱不能为空');
await expect(page.locator('#password-error')).toHaveText('密码不能为空');
});
- ✅ 示例 4:打开弹窗并验证内容
test('打开弹窗并验证内容', async ({ page }) => {
await page.goto('https://yourapp.com');
await page.click('button.view-detail');
await page.waitForSelector('.modal-content');
await expect(page.locator('.modal-title')).toHaveText('详情信息');
});
- ✅ 示例 5:拦截并 Mock 接口返回数据
test('拦截接口并返回自定义数据', async ({ page }) => {
await page.route('**/api/user/info', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: '测试用户', id: 1 }),
})
);
await page.goto('https://yourapp.com/user');
await expect(page.locator('.username')).toHaveText('测试用户');
});
- ✅ 示例 6:截图用于 UI 回归测试
test('页面截图', async ({ page }) => {
await page.goto('https://yourapp.com/dashboard');
await page.screenshot({ path: 'screenshots/dashboard.png', fullPage: true });
});
- ✅ 示例 7:使用 test.step 添加步骤标记(增强 Trace 可读性)
test('使用步骤结构化流程', async ({ page }) => {
await test.step('打开登录页', async () => {
await page.goto('https://yourapp.com/login');
});
await test.step('填写登录表单', async () => {
await page.fill('#username', 'robbie');
await page.fill('#password', '123456');
});
await test.step('点击登录按钮并跳转', async () => {
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/dashboard/);
});
});
- ✅ 示例 8:使用 test.use 指定不同登录状态
Playwright 支持通过 test.use() 给某些 describe 套件设置专属上下文(例如已登录用户):
test.describe.use({ storageState: 'logged-in-state.json' });
test('已登录用户访问主页', async ({ page }) => {
await page.goto('https://yourapp.com');
await expect(page.locator('.username')).toHaveText('robbie');
});