目录

  1. 1. 开篇:测试工程师的至暗时刻
  2. 2. Playwright是什么?
  3. 3. 三强争霸:Playwright vs Cypress vs Selenium
  4. 4. 环境搭建:5分钟上手
  5. 5. 核心概念:Locator与Action
  6. 6. 实战:编写第一个E2E测试
  7. 7. Page Object模式最佳实践
  8. 8. 并行测试:效率提升5倍的秘密
  9. 9. CI/CD集成:GitHub Actions配置
  10. 10. 避坑指南:常见问题与解决方案
  11. 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('放这里'));

💡 效率技巧:优先使用getByRolegetByLabelgetByText等语义化定位器,而不是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. 1. 多浏览器:一套代码测Chrome、Firefox、Safari
  2. 2. 并行执行:workers配置让测试效率提升5倍
  3. 3. Page Object:封装页面逻辑,测试代码更易维护
  4. 4. CI/CD集成:GitHub Actions一行配置,自动化回归
  5. 5. 调试友好:Trace Viewer让问题定位事半功倍

如果你还在为手动测试烦恼,或者被Selenium的慢速度折磨,是时候试试Playwright了。5分钟上手,1小时见效,这才是现代前端工程师该有的效率工具。


CSDN标签:Playwright, E2E测试, 自动化测试, Selenium, Cypress, 前端测试, CI/CD

参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐