前端技术05-Selenium太慢?从手动测试到自动化:Playwright多浏览器并行测试实战,Playwright让E2E测试效率翻倍
目录
- 1. 开篇:测试工程师的至暗时刻
- 2. Playwright是什么?
- 3. 三强争霸:Playwright vs Cypress vs Selenium
- 4. 环境搭建:5分钟上手
- 5. 核心概念:Locator与Action
- 6. 实战:编写第一个E2E测试
- 7. Page Object模式最佳实践
- 8. 并行测试:效率提升5倍的秘密
- 9. CI/CD集成:GitHub Actions配置
- 10. 避坑指南:常见问题与解决方案
- 11. 文末三件套
开篇:测试工程师的至暗时刻
你是否遇到过每次上线都要手动测试一遍所有功能,重复劳动浪费大量时间的痛苦场景?Selenium配置复杂,跑个测试用例慢得想砸电脑。网上搜到的E2E方案要么代码写不动,要么浏览器兼容性差。本文将从原理到实战,给出一个生产级解决方案,包含完整代码和避坑指南。
💡 效率技巧:据统计,一个中等规模的前端项目,回归测试通常需要2-3人天。而自动化E2E测试可以将这个时间压缩到30分钟以内,人力成本降低90%。
想象一下:周五晚上6点,你正准备下班,产品经理突然说"有个紧急需求要上线"。你看着那200多个测试用例,心里默默算了算——手动测完大概要到凌晨2点。这时候,一套靠谱的自动化测试方案就是你的救命稻草。
Playwright是什么?
Playwright是微软开源的端到端(E2E)测试框架,2020年发布以来迅速成为前端测试领域的新宠。它支持Chromium、Firefox、WebKit三大浏览器引擎,可以用同一套代码测试Chrome、Edge、Firefox、Safari。
┌─────────────────────────────────────────────────────────────┐
│ Playwright 架构图 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Chrome │ │ Firefox │ │ Safari │ │
│ │ (Chromium) │ │ (Gecko) │ │ (WebKit) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ Playwright API │ │
│ │ (统一接口) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ 你的测试代码 │ │
│ │ (JS/TS/Python) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
核心优势一览
| 特性 | Playwright | 说明 |
|---|---|---|
| 多浏览器 | ✅ Chromium/Firefox/WebKit | 真正的跨浏览器测试 |
| 并行执行 | ✅ 原生支持 | 多worker并行,效率翻倍 |
| 自动等待 | ✅ 内置智能等待 | 告别sleep(3000) |
| 录制回放 | ✅ Codegen工具 | 点点鼠标生成代码 |
| 移动端模拟 | ✅ 内置设备列表 | iPhone、Pixel一键模拟 |
| API测试 | ✅ 支持 | 同一框架测接口和UI |
| 可视化调试 | ✅ Trace Viewer | 慢动作回放测试过程 |
| CI/CD友好 | ✅ 一行命令跑完 | Docker、GitHub Actions无缝集成 |
⚠️ 避坑警告:Playwright的WebKit支持在Windows上需要额外安装依赖,Linux上需要安装系统库。建议用官方Docker镜像或在macOS上开发。
三强争霸:Playwright vs Cypress vs Selenium
选测试框架就像选对象——没有最好的,只有最适合的。我们来做个全面对比:
┌────────────────────────────────────────────────────────────────────┐
│ 三大框架对比矩阵 │
├──────────────┬─────────────┬─────────────┬─────────────────────────┤
│ 特性 │ Playwright │ Cypress │ Selenium │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 浏览器支持 │ Chromium │ Chromium │ Chrome/Firefox/ │
│ │ Firefox │ (核心) │ Safari/Edge/IE... │
│ │ WebKit │ Firefox │ │
│ │ │ (实验性) │ │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 执行速度 │ ⚡⚡⚡ │ ⚡⚡⚡ │ ⚡ │
│ │ (并行快) │ (单线程快) │ (慢,尤其IE) │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 多标签页 │ ✅ │ ❌ │ ✅ │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ iframe支持 │ ✅ │ ⚠️ │ ✅ │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 移动端测试 │ ✅ │ ❌ │ ⚠️ (需Appium) │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 跨域测试 │ ✅ │ ❌ │ ✅ │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 语言支持 │ JS/TS/Python│ JS │ Java/Python/C#/JS... │
│ │ /Java │ │ │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 学习曲线 │ 平缓 │ 平缓 │ 陡峭 │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 社区活跃度 │ 🔥🔥🔥 │ 🔥🔥🔥 │ 🔥🔥 │
├──────────────┼─────────────┼─────────────┼─────────────────────────┤
│ 背后公司 │ 微软 │ Cypress.io │ Selenium项目(开源) │
└──────────────┴─────────────┴─────────────┴─────────────────────────┘
一句话总结
- • Playwright:功能全面、速度快、适合大型项目,微软背书,未来可期
- • Cypress:开发体验好、调试方便,但多标签页和跨域是硬伤
- • Selenium:老牌框架、生态庞大,但配置复杂、速度慢,适合遗留项目维护
💡 效率技巧:如果你的项目需要测试多标签页交互(比如支付回调、OAuth登录),直接选Playwright。Cypress在这块会让你怀疑人生。
环境搭建:5分钟上手
1. 安装Node.js
确保Node.js版本 >= 14:
node -v
2. 初始化项目
mkdir playwright-demo && cd playwright-demo
npm init -y
3. 安装Playwright
npm init playwright@latest
安装过程中会询问几个问题:
- • 选择TypeScript或JavaScript(推荐TypeScript)
- • 测试目录名(默认
tests) - • 是否添加GitHub Actions工作流(选Yes)
- • 是否安装浏览器(选Yes)
安装完成后,项目结构如下:
playwright-demo/
├── tests/
│ └── example.spec.ts # 示例测试文件
├── tests-examples/
│ └── demo-todo-app.spec.ts # TodoMVC完整示例
├── playwright.config.ts # 配置文件
├── package.json
└── package-lock.json
4. 运行示例测试
npx playwright test
第一次运行会自动下载浏览器二进制文件(Chromium、Firefox、WebKit),大概需要几分钟。
⚠️ 避坑警告:如果下载浏览器卡住,可以设置镜像源:
# Windows PowerShell $env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" npx playwright install
5. 查看测试报告
npx playwright show-report
核心概念:Locator与Action
Playwright的设计哲学是"以用户视角编写测试"。两个核心概念:
Locator(定位器)
Locator是用来找到页面元素的"指南针",支持多种定位策略:
// 1. 通过role定位(推荐,最符合无障碍标准)
const button = page.getByRole('button', { name: '提交' });
// 2. 通过文本定位
const link = page.getByText('忘记密码?');
// 3. 通过label定位(表单元素)
const input = page.getByLabel('用户名');
// 4. 通过placeholder定位
const search = page.getByPlaceholder('搜索...');
// 5. 通过test id定位(最稳定,不受文案变化影响)
const card = page.getByTestId('product-card');
// 6. CSS选择器(不推荐,但有时候不得不用)
const item = page.locator('.list-item:nth-child(3)');
// 7. XPath(万不得已再用)
const xpath = page.locator('xpath=//div[@class="modal"]');
Action(操作)
找到元素后,就可以对它进行操作:
// 点击
await page.getByRole('button').click();
// 双击
await page.getByText('打开').dblclick();
// 右键
await page.getByRole('button').click({ button: 'right' });
// 悬停
await page.getByText('菜单').hover();
// 输入文本
await page.getByLabel('用户名').fill('admin');
// 清空并输入
await page.getByLabel('搜索').clear();
await page.getByLabel('搜索').fill('Playwright');
// 键盘操作
await page.getByLabel('搜索').press('Enter');
await page.getByLabel('搜索').press('Control+a'); // 全选
await page.getByLabel('搜索').press('Control+c'); // 复制
// 选择下拉框
await page.getByLabel('省份').selectOption('北京市');
// 勾选复选框
await page.getByLabel('同意协议').check();
await page.getByLabel('同意协议').uncheck();
// 单选按钮
await page.getByLabel('男').check();
// 上传文件
await page.getByLabel('上传头像').setInputFiles('path/to/avatar.png');
// 拖拽
await page.getByText('拖拽我').dragTo(page.getByText('放这里'));
💡 效率技巧:优先使用
getByRole、getByLabel、getByText等语义化定位器,而不是CSS选择器。这样测试代码更健壮,页面样式调整也不容易挂。
实战:编写第一个E2E测试
假设我们要测试一个电商网站的登录和下单流程:
// tests/ecommerce.spec.ts
import { test, expect } from '@playwright/test';
test.describe('电商网站E2E测试', () => {
test.beforeEach(async ({ page }) => {
// 每个测试用例开始前执行
await page.goto('https://demo.ecommerce.com');
});
test('用户登录成功', async ({ page }) => {
// 1. 进入登录页
await page.getByRole('link', { name: '登录' }).click();
// 2. 填写表单
await page.getByLabel('邮箱').fill('test@example.com');
await page.getByLabel('密码').fill('password123');
// 3. 点击登录
await page.getByRole('button', { name: '登录' }).click();
// 4. 验证登录成功
await expect(page.getByText('欢迎回来')).toBeVisible();
await expect(page.getByText('test@example.com')).toBeVisible();
});
test('用户购买商品完整流程', async ({ page }) => {
// 1. 搜索商品
await page.getByPlaceholder('搜索商品').fill('iPhone 15');
await page.getByRole('button', { name: '搜索' }).click();
// 2. 选择第一个商品
await page.getByTestId('product-card').first().click();
// 3. 加入购物车
await page.getByRole('button', { name: '加入购物车' }).click();
// 4. 验证提示
await expect(page.getByText('已成功加入购物车')).toBeVisible();
// 5. 进入购物车
await page.getByRole('link', { name: '购物车' }).click();
// 6. 结算
await page.getByRole('button', { name: '去结算' }).click();
// 7. 填写收货地址
await page.getByLabel('收件人').fill('张三');
await page.getByLabel('手机号').fill('13800138000');
await page.getByLabel('详细地址').fill('北京市朝阳区xxx街道');
// 8. 提交订单
await page.getByRole('button', { name: '提交订单' }).click();
// 9. 验证订单成功
await expect(page.getByText('订单提交成功')).toBeVisible();
await expect(page.getByTestId('order-id')).toBeVisible();
});
test('购物车数量计算正确', async ({ page }) => {
// 添加3个商品到购物车
for (let i = 0; i < 3; i++) {
await page.goto(`https://demo.ecommerce.com/product/${i + 1}`);
await page.getByRole('button', { name: '加入购物车' }).click();
await page.waitForTimeout(500); // 等待提示消失
}
// 验证购物车角标显示3
const cartBadge = page.getByTestId('cart-badge');
await expect(cartBadge).toHaveText('3');
});
});
运行单个测试文件
npx playwright test tests/ecommerce.spec.ts
带UI调试模式运行
npx playwright test --ui
这会打开一个可视化的测试浏览器,你可以:
- • 单步执行测试
- • 查看每一步的DOM快照
- • 实时修改代码并重新运行
💡 效率技巧:调试时加
--headed参数可以看到浏览器窗口,方便观察测试执行过程。CI环境默认是无头模式(headless)。
Page Object模式最佳实践
当测试用例多了,直接操作DOM会让代码难以维护。Page Object模式把页面封装成类,测试代码只和页面对象交互。
┌─────────────────────────────────────────────────────────────────┐
│ Page Object 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ 测试用例 │ ─────▶ │ Page Object 层 │ │
│ │ (test.spec.ts) │ │ (pages/login.page.ts) │ │
│ │ │ │ │ │
│ │ test('登录', │ │ class LoginPage { │ │
│ │ async () => { │ │ async login() { ... } │ │
│ │ await │ │ async goto() { ... } │ │
│ │ loginPage │ │ } │ │
│ │ .login() │ │ │ │
│ │ }) │ └─────────────────────────────┘ │
│ └─────────────────┘ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Playwright API │ │
│ │ (page.locator/click/fill) │ │
│ └─────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 浏览器 │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
实现Page Object
// pages/base.page.ts
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
async goto(url: string) {
await this.page.goto(url);
}
async waitForLoad() {
await this.page.waitForLoadState('networkidle');
}
}
// pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class LoginPage extends BasePage {
// Locators
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
readonly userMenu: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('邮箱');
this.passwordInput = page.getByLabel('密码');
this.loginButton = page.getByRole('button', { name: '登录' });
this.errorMessage = page.getByTestId('error-message');
this.userMenu = page.getByTestId('user-menu');
}
async goto() {
await super.goto('https://demo.ecommerce.com/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectLoginSuccess() {
await expect(this.userMenu).toBeVisible();
}
async expectLoginFailure(errorText?: string) {
await expect(this.errorMessage).toBeVisible();
if (errorText) {
await expect(this.errorMessage).toHaveText(errorText);
}
}
}
// pages/product.page.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './base.page';
export class ProductPage extends BasePage {
readonly addToCartButton: Locator;
readonly productName: Locator;
readonly productPrice: Locator;
readonly quantityInput: Locator;
constructor(page: Page) {
super(page);
this.addToCartButton = page.getByRole('button', { name: '加入购物车' });
this.productName = page.getByTestId('product-name');
this.productPrice = page.getByTestId('product-price');
this.quantityInput = page.getByLabel('数量');
}
async goto(productId: string) {
await super.goto(`https://demo.ecommerce.com/product/${productId}`);
}
async addToCart(quantity: number = 1) {
if (quantity > 1) {
await this.quantityInput.fill(quantity.toString());
}
await this.addToCartButton.click();
}
async getProductInfo() {
return {
name: await this.productName.textContent(),
price: await this.productPrice.textContent(),
};
}
}
// pages/cart.page.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class CartPage extends BasePage {
readonly checkoutButton: Locator;
readonly cartItems: Locator;
readonly totalPrice: Locator;
readonly emptyCartMessage: Locator;
constructor(page: Page) {
super(page);
this.checkoutButton = page.getByRole('button', { name: '去结算' });
this.cartItems = page.getByTestId('cart-item');
this.totalPrice = page.getByTestId('total-price');
this.emptyCartMessage = page.getByText('购物车是空的');
}
async goto() {
await super.goto('https://demo.ecommerce.com/cart');
}
async getItemCount() {
return await this.cartItems.count();
}
async removeItem(index: number) {
const item = this.cartItems.nth(index);
await item.getByRole('button', { name: '删除' }).click();
}
async proceedToCheckout() {
await this.checkoutButton.click();
}
async expectEmptyCart() {
await expect(this.emptyCartMessage).toBeVisible();
}
}
使用Page Object编写测试
// tests/pom-example.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { ProductPage } from '../pages/product.page';
import { CartPage } from '../pages/cart.page';
test.describe('使用Page Object的测试', () => {
test('完整购买流程', async ({ page }) => {
// 1. 登录
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('test@example.com', 'password123');
await loginPage.expectLoginSuccess();
// 2. 浏览商品并加入购物车
const productPage = new ProductPage(page);
await productPage.goto('iphone-15');
const { name, price } = await productPage.getProductInfo();
await productPage.addToCart(2);
// 3. 查看购物车
const cartPage = new CartPage(page);
await cartPage.goto();
expect(await cartPage.getItemCount()).toBe(1);
// 4. 结算
await cartPage.proceedToCheckout();
// ... 继续后续流程
});
test('未登录用户无法结算', async ({ page }) => {
const cartPage = new CartPage(page);
await cartPage.goto();
await cartPage.expectEmptyCart();
});
});
💡 效率技巧:Page Object只封装"做什么"(业务操作),不封装"怎么做"(具体定位)。如果一个按钮从"提交"改成"确认提交",只需要改Page Object,测试用例完全不用动。
并行测试:效率提升5倍的秘密
Playwright的并行能力是其最大杀手锏之一。默认配置下,测试是串行执行的,但开启并行后,效率能提升5倍以上。
工作原理
串行执行(默认) 并行执行(workers=4)
Test 1 ──────────────────────▶ Worker 1: Test 1 ────▶
Test 2 ──────────────────────▶ Worker 2: Test 2 ────▶
Test 3 ──────────────────────▶ Worker 3: Test 3 ────▶
Test 4 ──────────────────────▶ Worker 4: Test 4 ────▶
Test 5 ──────────────────────▶ Worker 1: Test 5 ────▶
Test 6 ──────────────────────▶ Worker 2: Test 6 ────▶
总时间 = 6 × T 总时间 ≈ 2 × T
配置并行测试
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// 并行配置
fullyParallel: true, // 完全并行模式
workers: process.env.CI ? 4 : undefined, // CI环境4个worker,本地自动
// 每个worker的浏览器配置
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环境)
retries: process.env.CI ? 2 : 0,
// 超时设置
timeout: 30 * 1000, // 30秒
expect: {
timeout: 5000, // 断言超时5秒
},
});
控制并行粒度
有时候你需要控制哪些测试可以并行,哪些必须串行:
// tests/parallel.spec.ts
import { test, expect } from '@playwright/test';
// 默认并行执行
test('测试A', async ({ page }) => {
// ...
});
test('测试B', async ({ page }) => {
// ...
});
// 同一个文件内串行执行
test.describe.serial('订单相关测试(需要串行)', () => {
test('创建订单', async ({ page }) => {
// ...
});
test('支付订单', async ({ page }) => {
// 依赖上一个测试创建的订单
});
test('取消订单', async ({ page }) => {
// 依赖上一个测试支付的订单
});
});
// 完全禁用并行(单个worker执行)
test.describe.configure({ mode: 'serial' });
数据隔离:每个测试独立环境
并行测试最大的挑战是数据隔离。Playwright推荐每个测试用独立的数据:
// 使用API创建测试数据
test('创建用户并测试', async ({ page, request }) => {
// 1. 通过API创建测试用户
const uniqueEmail = `test-${Date.now()}@example.com`;
await request.post('/api/users', {
data: { email: uniqueEmail, password: 'test123' }
});
// 2. 用新用户登录
await page.goto('/login');
await page.getByLabel('邮箱').fill(uniqueEmail);
// ...
// 3. 测试结束后清理(或用beforeEach统一清理)
});
⚠️ 避坑警告:并行测试时,多个worker可能同时操作同一数据,导致 flaky test(不稳定测试)。务必确保每个测试用独立的数据集,或使用事务回滚机制。
CI/CD集成:GitHub Actions配置
自动化测试的价值在CI/CD中才能最大化。Playwright官方提供了GitHub Actions模板:
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
BASE_URL: ${{ secrets.TEST_BASE_URL }}
TEST_USER: ${{ secrets.TEST_USER }}
TEST_PASS: ${{ secrets.TEST_PASS }}
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 30
多浏览器矩阵测试
# .github/workflows/playwright-matrix.yml
name: Cross-Browser Tests
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * 1' # 每周一凌晨2点运行
jobs:
test:
strategy:
fail-fast: false
matrix:
project: [chromium, firefox, webkit]
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run tests (${{ matrix.project }} on ${{ matrix.os }})
run: npx playwright test --project=${{ matrix.project }}
- name: Upload results
uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ matrix.project }}-${{ matrix.os }}-results
path: test-results/
Docker环境运行
# Dockerfile.test
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
# 构建并运行
docker build -f Dockerfile.test -t playwright-tests .
docker run --rm -v $(pwd)/test-results:/app/test-results playwright-tests
💡 效率技巧:CI环境用
--reporter=html,line同时生成HTML报告和控制台输出,方便排查问题。HTML报告可以上传到GitHub Pages或S3做长期存档。
避坑指南:常见问题与解决方案
1. 测试不稳定(Flaky Tests)
症状:同样的测试,有时候过有时候不过
原因:
- • 网络请求未完成就开始断言
- • 动画/过渡效果导致元素位置变化
- • 测试数据被其他测试污染
解决方案:
// ❌ 错误:硬编码等待
await page.waitForTimeout(3000);
await expect(page.getByText('加载完成')).toBeVisible();
// ✅ 正确:使用自动等待
await expect(page.getByText('加载完成')).toBeVisible({ timeout: 10000 });
// ✅ 更好:等待网络请求完成
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
await expect(page.getByText('数据已加载')).toBeVisible();
2. 弹窗/对话框处理
// 处理alert/confirm/prompt
test('处理确认对话框', async ({ page }) => {
// 监听对话框事件
page.on('dialog', async dialog => {
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toBe('确定要删除吗?');
await dialog.accept(); // 点击确定
// await dialog.dismiss(); // 点击取消
});
await page.getByRole('button', { name: '删除' }).click();
});
3. 文件下载测试
test('下载文件', async ({ page }) => {
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: '导出' }).click(),
]);
expect(download.suggestedFilename()).toBe('report.pdf');
await download.saveAs('/tmp/' + download.suggestedFilename());
});
4. 多标签页/窗口测试
test('新窗口打开链接', async ({ page, context }) => {
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.getByRole('link', { name: '在新窗口打开' }).click(),
]);
await newPage.waitForLoadState();
await expect(newPage).toHaveURL(/\/detail/);
await expect(newPage.getByText('详情页')).toBeVisible();
});
5. 认证状态复用
// playwright.config.ts
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
});
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
// 登录并保存状态
await page.goto('https://demo.ecommerce.com/login');
await page.getByLabel('邮箱').fill('test@example.com');
await page.getByLabel('密码').fill('password123');
await page.getByRole('button', { name: '登录' }).click();
// 保存认证状态到文件
await page.context().storageState({ path: 'auth.json' });
await browser.close();
}
export default globalSetup;
// 测试文件中使用保存的状态
test.use({ storageState: 'auth.json' });
test('已登录用户测试', async ({ page }) => {
// 直接进入已登录状态,无需重新登录
await page.goto('/dashboard');
await expect(page.getByText('欢迎回来')).toBeVisible();
});
6. 截图和录屏
// 失败时自动截图
export default defineConfig({
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
},
});
// 手动截图
test('手动截图', async ({ page }) => {
await page.goto('/dashboard');
await page.screenshot({ path: 'dashboard.png', fullPage: true });
});
⚠️ 避坑警告:不要在测试代码里用
console.log调试,Playwright有自己的日志系统。用DEBUG=pw:api npx playwright test可以看到详细的API调用日志。
文末三件套
1. 【源码获取】
关注此系列获取后续更新,后台回复'Playwright'获取完整源码和示例项目链接。
项目包含:
- • 完整的Page Object示例
- • GitHub Actions配置模板
- • Docker运行配置
- • 常见测试场景代码片段
2. 【思考题】
你的团队E2E测试覆盖率如何?
- • A. 还没有E2E测试,全靠手动
- • B. 有少量测试,覆盖率<30%
- • C. 核心流程已覆盖,覆盖率50%左右
- • D. 覆盖率>80%,每次发布都跑全量回归
欢迎在评论区分享你的答案和踩坑经历!
3. 【系列预告】
下一篇《GitHub Actions CI/CD实战》将深入讲解:
- • 如何配置多环境部署流水线
- • 自动化测试与代码审查的集成
- • 测试报告可视化与告警机制
- • 性能测试与E2E测试的结合
总结
Playwright作为新一代E2E测试框架,凭借多浏览器支持、原生并行能力、智能等待机制,正在快速取代Selenium和Cypress成为前端测试的首选。
核心要点回顾:
- 1. 多浏览器:一套代码测Chrome、Firefox、Safari
- 2. 并行执行:workers配置让测试效率提升5倍
- 3. Page Object:封装页面逻辑,测试代码更易维护
- 4. CI/CD集成:GitHub Actions一行配置,自动化回归
- 5. 调试友好:Trace Viewer让问题定位事半功倍
如果你还在为手动测试烦恼,或者被Selenium的慢速度折磨,是时候试试Playwright了。5分钟上手,1小时见效,这才是现代前端工程师该有的效率工具。
CSDN标签:Playwright, E2E测试, 自动化测试, Selenium, Cypress, 前端测试, CI/CD
参考链接:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)