前言:为什么选择 Playwright?

在 Web 自动化测试领域,Selenium 曾长期占据主流地位,但面对现代前端框架(React/Vue/Next.js)、复杂 SPA 应用和多端适配需求,其局限性逐渐凸显。很多测试团队仍然在 Selenium 的“玄学失败”中苦苦挣扎——今天跑通的脚本明天莫名失败,排查半天发现只是网络慢了 200ms,至少 30% 的时间花在“救火”上而不是设计更有效的测试。

Microsoft 推出的 Playwright 框架,凭借跨引擎、跨平台、智能化的特性,正迅速成为新一代自动化测试的优选方案。它不仅支持 Chromium、Firefox、WebKit 三大引擎和 JavaScript/TypeScript/Python/Java/.NET 多语言,更内置了智能等待机制,可以自动等待元素可交互,彻底告别 sleep() 硬编码。

今天,我们将从零开始,系统地学习 Playwright 的全部核心知识点。本文适合:正在使用或准备使用 Playwright 做自动化测试的朋友,无论你是测试新人还是资深工程师,都能从中获得有价值的实战经验。

阅读提示:本文约 2 万字,建议收藏后分章节阅读。每章末尾都附有实战小结,帮助你快速回顾要点。

第一章:Playwright 核心优势深度解析

1.1 为什么 Playwright 比 Selenium 快?

很多人问:“都是控制浏览器,差别真有那么大?”答案藏在通信机制里。

Selenium:三层转发,层层损耗

你的代码 → WebDriver 协议 → 浏览器驱动 → 浏览器

每一步都有序列化、反序列化、网络延迟。就像寄快递,中间经手人越多,越容易丢件、延误。Selenium 基于 HTTP 协议与浏览器 Driver 通信,每次操作需重建连接,平均延迟超过 500ms。

Playwright:直连 DevTools Protocol

你的代码 → 浏览器(通过 CDP)

Playwright 采用进程外通信模型,通过 WebSocket 协议与浏览器驱动交互,保持持久化连接,没有中间代理,直接与浏览器内核对话。实测数据表明,单次操作平均延迟控制在 200ms 以内,较 Selenium 效率提升超 60%。

在一个典型场景(打开电商首页 → 搜索商品 → 加入购物车,重复 100 次)的实测中:

指标SeleniumPlaywright提升
平均耗时4.2s1.3s3.2 倍
标准差0.8s0.1s稳定性大幅提升

这不仅是“快一点”,而是质的飞跃。

1.2 核心特性一览

Playwright 的核心特性使其成为现代 Web 自动化测试的理想选择:

1. 跨浏览器测试能力

支持 Chromium、Firefox、WebKit 三大引擎,一套 API 搞定所有浏览器。原生集成三大内核,无需独立 Driver 管理,彻底解决了跨浏览器测试的环境配置难题。

2. 多语言支持

提供 JavaScript、TypeScript、Python、.NET 和 Java 的原生支持。

3. 智能等待机制

Playwright 的操作前自动检测元素可见性、可操作性等四重状态(可见性、可操作性、稳定性、交互就绪),减少 30% 因异步加载导致的测试失败。

4. 无头和有头模式

可在可见或后台模式之间灵活切换。

5. 移动设备模拟

无缝模拟设备、屏幕尺寸和地理位置。

6. 网络拦截和 Mock

支持拦截 API 调用并自定义响应,无需启动真实后端服务即可测试各种场景。

7. 全链路监控

Trace Viewer 可记录操作视频、网络请求和 DOM 快照,实现分钟级故障定位。

1.3 核心架构解析

理解 Playwright 的架构对于写出高效稳定的测试至关重要。

多进程并行执行模型

Playwright 采用浏览器实例级并行架构,每个测试用例在独立的浏览器上下文中运行,通过进程隔离彻底消除资源竞争。对比传统框架的线程级并行方案,其资源利用率提升 3-5 倍,特别适合微服务架构下的端到端测试场景。

全自动等待机制

通过内置智能等待策略,Playwright 可自动处理异步加载、动画过渡等动态页面行为。其独创的 “actionability” 检查系统,包含 12 种状态验证条件(如可见性、可点击性、稳定性等),将传统测试中需要手动添加的显式等待代码量减少 80% 以上。

第一章小结:Playwright 的架构优势从根本上解决了 Selenium 时代的三大痛点——执行慢、脚本不稳定、维护成本高。理解这些架构特性,是写出高质量 Playwright 测试的基础。

第二章:环境搭建与快速入门

2.1 安装 Playwright

在已有 Node.js 环境的项目中,执行以下命令一键初始化 Playwright:

npm init playwright@latest

执行后,脚本会引导你完成以下配置:

  • 选择使用 TypeScript 还是 JavaScript
  • 指定测试文件存放目录(默认 tests
  • 是否添加 GitHub Actions 工作流
  • 是否安装 Playwright 浏览器(建议选择是,否则后续需要手动运行 npx playwright install

如果你已经有一个 Node.js 项目,也可以手动安装:

# 创建项目目录
mkdir playwright-demo
cd playwright-demo

# 初始化项目
npm init -y

# 安装 Playwright 测试包
npm install @playwright/test --save-dev

# 安装浏览器驱动
npx playwright install

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

playwright-demo/
├── playwright.config.ts    # 配置文件
├── package.json
├── tests/                  # 测试文件目录
│   └── example.spec.ts     # 示例测试
├── tests-examples/         # 更多示例
└── node_modules/

2.2 编写第一个测试

Playwright 的 API 非常直观。让我们在 tests/first.spec.ts 中编写第一个测试:

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

test('验证 Playwright 官网标题', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  
  // 期待标题包含 "Playwright"
  await expect(page).toHaveTitle(/Playwright/);
});

test('点击 Get started 链接', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  
  // 使用语义化角色定位器点击链接
  await page.getByRole('link', { name: 'Get started' }).click();
  
  // 期待 URL 包含 intro
  await expect(page).toHaveURL(/.*intro/);
});

运行测试:

npx playwright test

运行特定文件:

npx playwright test tests/first.spec.ts

使用 UI 模式运行(强烈推荐初学者使用):

npx playwright test --ui

2.3 核心概念:Browser、Context 和 Page

理解这三个核心对象的关系是掌握 Playwright 的关键:

const { chromium } = require('playwright');

(async () => {
  // 1. 启动浏览器实例
  const browser = await chromium.launch();
  
  // 2. 创建浏览器上下文(相当于一个独立的浏览器会话)
  const context = await browser.newContext();
  
  // 3. 在上下文中创建新页面
  const page = await context.newPage();
  
  await page.goto('https://playwright.dev/');
  
  // 关闭浏览器
  await browser.close();
})();

Browser:代表一个浏览器实例(如 Chrome、Firefox)。可以创建多个 Context。

Browser Context:相当于一个独立的浏览器会话。不同 Context 之间的 Cookie、LocalStorage 完全隔离,这为并行测试提供了天然优势。

Page:代表一个浏览器标签页,所有页面交互都在 Page 对象上进行。

第二章小结:Playwright 的安装和初始配置极其简单,npm init playwright@latest 一条命令就能完成所有配置。理解 Browser → Context → Page 的三层模型,是驾驭 Playwright 的第一步。

第三章:元素定位 —— 决定测试稳定性的核心

元素定位是自动化测试中最基础也最关键的一环。定位策略的选择直接影响测试脚本的稳定性和可维护性。一个不稳定的定位器,会让你的测试变得像“薛定谔的猫”——时而成功时而失败。

Playwright 提供了丰富且强大的定位 API,本章将详细介绍 8 种核心定位策略。

3.1 通过文本定位 —— 最直观的方式

当元素有清晰的文本内容时,通过文本定位是最直观的方式:

// 点击包含特定文本的按钮
await page.click('text=立即购买');

// 更精确的文本匹配(完全匹配)
await page.click('text="登录"');

// 文本包含特定内容(使用正则)
await page.click('text=/忘记密码/');

// 匹配包含"商品"的任意元素
await page.click('text=/商品/');

3.2 CSS 选择器 —— 最灵活的方式

CSS 选择器是日常使用最频繁的定位方式,尤其是在处理复杂页面时:

// 通过 ID
await page.click('#login-button');

// 通过类名
await page.click('.submit-btn');

// 通过属性
await page.click('button[type="submit"]');

// 组合选择器
await page.click('div.container > form > button.primary');

// 伪类选择器
await page.click('input:disabled');

// 处理动态生成的类名(包含部分固定字符)
await page.click('[class*="btn-primary"]');

3.3 Role 定位 —— 语义化的最佳实践

这是 Playwright 的特色功能,基于 ARIA 角色进行定位,让测试更具可访问性:

// 使用 role 属性进行精确定位
await page.getByRole('button', { name: '提交' }).click();
await page.getByRole('link', { name: '帮助中心' }).click();
await page.getByRole('textbox', { name: '用户名' }).fill('testuser');
await page.getByRole('checkbox').check();

最佳实践:优先使用 role 定位,这能使你的测试代码更贴近用户的实际体验。

3.4 data-testid —— 团队协作的首选

这是最推荐的定位策略之一,尤其适合团队协作项目。通过在元素上添加专门的测试属性,可以实现最稳定的定位:

<!-- 开发人员在元素上添加测试属性 -->
<button data-testid="login-submit">登录</button>
<input data-testid="username-input" type="text" />
// 测试代码中使用
await page.getByTestId('login-submit').click();
await page.getByTestId('username-input').fill('admin');

团队协作建议:与开发团队约定统一的 data-testid 命名规范,这样可以有效避免因样式或结构调整导致的测试失败。将测试 ID 作为代码与测试的“契约”,在 PR 检查中要求核心 UI 元素必须加测试 ID。

3.5 Label 文本 —— 处理表单的最佳选择

处理表单时,通过关联的 label 文本定位是最佳选择:

<label for="email">邮箱地址</label>
<input type="email" id="email" />
// 通过 label 文本定位输入框
await page.getByLabel('邮箱地址').fill('test@example.com');

// 精确匹配
await page.getByLabel('密码', { exact: true }).fill('password123');

3.6 XPath 定位 —— 兜底方案

当其他定位方式都无法满足需求时,XPath 是最后的武器:

// 通过 XPath 定位
await page.locator('xpath=//button[@class="submit-btn"]').click();

// 复杂的 XPath 表达式
await page.locator('//div[@id="container"]//button[contains(text(), "确认")]').click();

注意:XPath 定位性能较差且容易受 DOM 结构变化影响,应作为兜底方案。

3.7 定位策略优先级指南

根据实际项目经验,推荐按照以下优先级选择定位策略:

优先级定位策略使用场景
1data-testid团队协作项目,核心业务元素
2getByRole语义化元素(按钮、链接、输入框等)
3getByLabel表单输入字段
4getByText静态文本内容
5CSS 选择器复杂结构定位
6XPath兜底方案

3.8 组合定位与过滤

Playwright 支持强大的组合定位语法:

// 文本选择器 - 点击文本为"Login"的按钮
await page.click("text=Login");

// CSS 选择器 + 文本组合
await page.click(".btn:has-text('Submit')");

// 选择第 N 个元素
await page.click("li >> nth=2");  // 选择第三个 li 元素

// 过滤:选择包含特定文本的 div 下的按钮
await page.click("div:has-text('Welcome') >> .btn");

第三章小结:稳定的定位策略是自动化测试的生命线。优先使用 data-testidgetByRole,与开发团队协作建立统一的定位规范,可以有效避免因页面变化导致的测试失败。

第四章:常用操作与断言

4.1 页面导航与交互

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

test('基础交互操作示例', async ({ page }) => {
  // 导航到页面
  await page.goto('https://example.com/login');
  
  // 填充表单
  await page.getByLabel('用户名').fill('admin');
  await page.getByLabel('密码').fill('123456');
  
  // 点击按钮
  await page.getByRole('button', { name: '登录' }).click();
  
  // 选择下拉框
  await page.selectOption('select#country', 'China');
  
  // 勾选复选框
  await page.getByRole('checkbox').check();
  
  // 取消勾选
  await page.getByRole('checkbox').uncheck();
  
  // 悬停
  await page.getByRole('button').hover();
  
  // 双击
  await page.getByText('双击我').dblclick();
  
  // 右键点击
  await page.getByText('右键菜单').click({ button: 'right' });
  
  // 键盘操作
  await page.keyboard.press('Enter');
  await page.keyboard.type('Hello World');
  
  // 上传文件
  await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
});

4.2 等待策略 —— 告别 sleep

Playwright 的智能等待是其最强大的特性之一。几乎所有操作都内置了自动等待机制:

// ✅ Playwright 正确方式
test('智能等待示例', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 以下操作会自动等待元素出现、可见、可操作
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'Password123');
  await page.click('#login');
  
  // 无需手动等待页面跳转,直接断言新页面元素
  await expect(page.locator('text=欢迎回来')).toBeVisible();
});

对比传统方式:

# ❌ Selenium 传统方式(错误示范)
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com")
# 需要大量 try-catch 和显式等待,代码冗长易失败
element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.ID, "username"))
)
element.send_keys("testuser")
# ...

当需要等待特定条件时,可以使用以下方式:

// 等待特定的网络请求完成
const [response] = await Promise.all([
  page.waitForResponse(resp => 
    resp.url().includes('/api/login') && resp.status() === 200
  ),
  page.getByRole('button', { name: '登录' }).click(),
]);

// 等待元素出现
await page.waitForSelector('.result-item');

// 等待导航完成
await page.waitForURL('**/dashboard');

// 等待函数返回 true
await page.waitForFunction(() => document.readyState === 'complete');

重要提示:放弃固定 sleep(waitForTimeout),这是“坏味道”,会增加测试抖动。优先等待网络完成、元素可见或路由稳定。

4.3 断言

Playwright 提供了丰富的断言方法:

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

test('断言示例', async ({ page }) => {
  await page.goto('https://example.com');
  
  // 页面标题断言
  await expect(page).toHaveTitle('Example Domain');
  
  // URL 断言
  await expect(page).toHaveURL('https://example.com/');
  await expect(page).toHaveURL(/.*example.*/);
  
  // 元素可见性断言
  await expect(page.locator('h1')).toBeVisible();
  
  // 元素文本断言
  await expect(page.locator('.message')).toHaveText('操作成功');
  await expect(page.locator('.message')).toContainText('成功');
  
  // 元素值断言
  await expect(page.locator('input')).toHaveValue('test');
  
  // 元素数量断言
  await expect(page.locator('.item')).toHaveCount(5);
  
  // 元素属性断言
  await expect(page.locator('button')).toBeEnabled();
  await expect(page.locator('button')).toBeDisabled();
  await expect(page.locator('input')).toBeChecked();
  
  // 截图对比(视觉回归)
  await expect(page).toHaveScreenshot('homepage.png');
});

第四章小结:Playwright 的智能等待机制是提高测试稳定性的关键。掌握丰富的断言方法,可以让测试验证更加精确和可靠。

第五章:实战案例 —— 电商网站端到端测试

本章通过一个完整的电商网站测试案例,将前面学到的知识串联起来。场景包括:用户登录、搜索商品、添加购物车、下单。

5.1 测试场景设计

场景:用户购买商品流程
1. 用户登录系统
2. 搜索商品
3. 查看商品详情
4. 添加商品到购物车
5. 进入购物车确认
6. 填写收货信息
7. 提交订单
8. 验证订单创建成功

5.2 使用 Page Object Model (POM) 组织代码

采用分层架构设计测试代码,是构建可维护测试框架的最佳实践:

基础层:封装通用方法

// pages/base.page.ts
import { Page } from '@playwright/test';

export class BasePage {
  protected page: Page;
  
  constructor(page: Page) {
    this.page = page;
  }
  
  async navigate(url: string) {
    await this.page.goto(url, { waitUntil: 'domcontentloaded' });
  }
  
  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }
  
  async takeScreenshot(name: string) {
    await this.page.screenshot({ path: `screenshots/${name}.png` });
  }
}

页面层:定义各页面专属元素与交互逻辑

// pages/login.page.ts
import { BasePage } from './base.page';
import { Page } from '@playwright/test';

export class LoginPage extends BasePage {
  // 元素定位器
  private usernameInput = () => this.page.getByTestId('username');
  private passwordInput = () => this.page.getByTestId('password');
  private loginButton = () => this.page.getByTestId('login-btn');
  private errorMessage = () => this.page.locator('.error-message');
  
  constructor(page: Page) {
    super(page);
  }
  
  async goto() {
    await this.navigate('/login');
  }
  
  async login(username: string, password: string) {
    await this.usernameInput().fill(username);
    await this.passwordInput().fill(password);
    await this.loginButton().click();
  }
  
  async getErrorMessage() {
    return await this.errorMessage().textContent();
  }
}
// pages/home.page.ts
import { BasePage } from './base.page';
import { Page } from '@playwright/test';

export class HomePage extends BasePage {
  private searchInput = () => this.page.getByTestId('search-input');
  private searchButton = () => this.page.getByTestId('search-btn');
  private productCards = () => this.page.locator('.product-card');
  
  constructor(page: Page) {
    super(page);
  }
  
  async searchProduct(keyword: string) {
    await this.searchInput().fill(keyword);
    await this.searchButton().click();
    await this.page.waitForURL(/.*search.*/);
  }
  
  async clickFirstProduct() {
    await this.productCards().first().click();
  }
}
// pages/product.page.ts
import { BasePage } from './base.page';
import { Page } from '@playwright/test';

export class ProductPage extends BasePage {
  private addToCartButton = () => this.page.getByTestId('add-to-cart');
  private productTitle = () => this.page.locator('.product-title');
  private productPrice = () => this.page.locator('.product-price');
  private quantityInput = () => this.page.getByTestId('quantity');
  
  constructor(page: Page) {
    super(page);
  }
  
  async getProductInfo() {
    return {
      title: await this.productTitle().textContent(),
      price: await this.productPrice().textContent()
    };
  }
  
  async setQuantity(qty: number) {
    await this.quantityInput().fill(qty.toString());
  }
  
  async addToCart() {
    await this.addToCartButton().click();
    await expect(this.page.locator('.cart-notification')).toBeVisible();
  }
}
// pages/cart.page.ts
import { BasePage } from './base.page';
import { Page, expect } from '@playwright/test';

export class CartPage extends BasePage {
  private cartItems = () => this.page.locator('.cart-item');
  private checkoutButton = () => this.page.getByTestId('checkout-btn');
  private totalPrice = () => this.page.locator('.cart-total');
  
  constructor(page: Page) {
    super(page);
  }
  
  async verifyItemInCart(productTitle: string) {
    await expect(this.cartItems().filter({ hasText: productTitle })).toBeVisible();
  }
  
  async proceedToCheckout() {
    await this.checkoutButton().click();
    await this.page.waitForURL(/.*checkout.*/);
  }
}
// pages/checkout.page.ts
import { BasePage } from './base.page';
import { Page, expect } from '@playwright/test';

export class CheckoutPage extends BasePage {
  private nameInput = () => this.page.getByTestId('name');
  private addressInput = () => this.page.getByTestId('address');
  private phoneInput = () => this.page.getByTestId('phone');
  private submitOrderButton = () => this.page.getByTestId('submit-order');
  private orderSuccessMessage = () => this.page.locator('.order-success');
  
  constructor(page: Page) {
    super(page);
  }
  
  async fillShippingInfo(info: { name: string; address: string; phone: string }) {
    await this.nameInput().fill(info.name);
    await this.addressInput().fill(info.address);
    await this.phoneInput().fill(info.phone);
  }
  
  async submitOrder() {
    await this.submitOrderButton().click();
  }
  
  async verifyOrderSuccess() {
    await expect(this.orderSuccessMessage()).toBeVisible();
    await expect(this.orderSuccessMessage()).toContainText('订单提交成功');
  }
}

测试层:组合页面操作形成业务流测试

// tests/e2e/purchase-flow.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../pages/login.page';
import { HomePage } from '../../pages/home.page';
import { ProductPage } from '../../pages/product.page';
import { CartPage } from '../../pages/cart.page';
import { CheckoutPage } from '../../pages/checkout.page';

test.describe('电商购买完整流程测试', () => {
  test('用户购买商品 - 成功流程', async ({ page }) => {
    // 1. 初始化页面对象
    const loginPage = new LoginPage(page);
    const homePage = new HomePage(page);
    const productPage = new ProductPage(page);
    const cartPage = new CartPage(page);
    const checkoutPage = new CheckoutPage(page);
    
    // 2. 用户登录
    await loginPage.goto();
    await loginPage.login('testuser@example.com', 'Test123456');
    await expect(page).toHaveURL(/.*dashboard.*/);
    
    // 3. 搜索商品
    await homePage.searchProduct('iPhone 15');
    
    // 4. 点击第一个商品进入详情
    await homePage.clickFirstProduct();
    await expect(page).toHaveURL(/.*product.*/);
    
    // 5. 获取商品信息并加入购物车
    const productInfo = await productPage.getProductInfo();
    await productPage.setQuantity(2);
    await productPage.addToCart();
    
    // 6. 进入购物车验证
    await cartPage.navigate('/cart');
    await cartPage.verifyItemInCart(productInfo.title!);
    await cartPage.proceedToCheckout();
    
    // 7. 填写收货信息并提交订单
    await checkoutPage.fillShippingInfo({
      name: '张三',
      address: '北京市朝阳区xxx路xxx号',
      phone: '13800138000'
    });
    await checkoutPage.submitOrder();
    
    // 8. 验证订单成功
    await checkoutPage.verifyOrderSuccess();
  });
});

5.3 运行测试

# 运行所有测试
npx playwright test

# 运行特定测试文件
npx playwright test tests/e2e/purchase-flow.spec.ts

# 使用有头模式运行(方便调试)
npx playwright test --headed

# 使用 UI 模式
npx playwright test --ui

第五章小结:通过 Page Object Model 设计模式,我们将页面元素和操作封装在独立的类中,使测试代码更加清晰、可维护。当页面发生变化时,只需修改对应的 Page 类,测试用例无需改动。

第六章:高级特性与进阶技巧

6.1 网络拦截与 Mock

网络拦截是 Playwright 最强大的特性之一。无需启动后端服务,直接拦截和修改 API 请求,测试极端场景快如闪电。

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

test('网络拦截示例 - Mock API 响应', async ({ page }) => {
  // 拦截特定 URL 的请求
  await page.route('**/api/users', async (route) => {
    // 模拟返回自定义 JSON 响应
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        users: [
          { id: 1, name: '张三' },
          { id: 2, name: '李四' }
        ]
      })
    });
  });
  
  await page.goto('https://example.com/users');
  // 页面将显示 Mock 的数据
});

test('网络拦截示例 - 修改请求', async ({ page }) => {
  // 拦截并修改请求头
  await page.route('**/api/**', async (route) => {
    const headers = {
      ...route.request().headers(),
      'X-Custom-Header': 'test-value'
    };
    await route.continue({ headers });
  });
  
  await page.goto('https://example.com');
});

test('网络拦截示例 - 阻止资源加载', async ({ page }) => {
  // 阻止图片加载,加速测试
  await page.route('**/*.{png,jpg,jpeg,gif}', route => route.abort());
  
  await page.goto('https://example.com');
  // 页面加载速度将大幅提升
});

test('网络拦截示例 - 监听请求', async ({ page }) => {
  const requests: string[] = [];
  
  page.on('request', request => {
    requests.push(request.url());
  });
  
  await page.goto('https://example.com');
  console.log('所有请求:', requests);
});

6.2 移动设备模拟

Playwright 内置了丰富的设备描述符,可以真实模拟移动设备环境:

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

test('iPhone 13 模拟', async ({ browser }) => {
  // 使用内置设备描述符
  const context = await browser.newContext({
    ...devices['iPhone 13'],
  });
  
  const page = await context.newPage();
  await page.goto('https://example.com');
  
  // 验证移动端适配
  await expect(page.locator('.mobile-menu')).toBeVisible();
});

test('自定义移动设备', async ({ browser }) => {
  const context = await browser.newContext({
    viewport: { width: 375, height: 812 },
    userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)',
    deviceScaleFactor: 2,
    isMobile: true,
    hasTouch: true,
  });
  
  const page = await context.newPage();
  await page.goto('https://example.com');
});

test('地理位置模拟', async ({ browser }) => {
  const context = await browser.newContext({
    geolocation: { longitude: 116.4074, latitude: 39.9042 },  // 北京坐标
    permissions: ['geolocation'],
  });
  
  const page = await context.newPage();
  await page.goto('https://example.com');
});

6.3 多标签页和多上下文

Playwright 轻松模拟多标签页、多用户场景,互不干扰:

test('多标签页操作', async ({ page, context }) => {
  await page.goto('https://example.com');
  
  // 点击链接在新标签页打开
  const [newPage] = await Promise.all([
    context.waitForEvent('page'),
    page.click('a[target="_blank"]')
  ]);
  
  // 在新标签页操作
  await newPage.waitForLoadState();
  await expect(newPage).toHaveTitle(/.*新页面.*/);
  
  // 切换回原标签页
  await page.bringToFront();
});

test('多上下文 - 模拟多用户', async ({ browser }) => {
  // 用户 A 的上下文
  const userAContext = await browser.newContext();
  const userAPage = await userAContext.newPage();
  await userAPage.goto('https://example.com/login');
  await userAPage.fill('#username', 'userA');
  await userAPage.fill('#password', 'passA');
  await userAPage.click('#login');
  
  // 用户 B 的上下文(完全隔离的 Cookie 和 Storage)
  const userBContext = await browser.newContext();
  const userBPage = await userBContext.newPage();
  await userBPage.goto('https://example.com/login');
  await userBPage.fill('#username', 'userB');
  await userBPage.fill('#password', 'passB');
  await userBPage.click('#login');
  
  // 两个用户会话完全独立,互不影响
});

6.4 API 测试

Playwright 不仅支持 UI 测试,还内置了 API 测试能力:

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

test.describe('API 测试', () => {
  test('GET 请求', async ({ request }) => {
    const response = await request.get('https://jsonplaceholder.typicode.com/posts/1');
    expect(response.status()).toBe(200);
    
    const data = await response.json();
    expect(data.id).toBe(1);
  });
  
  test('POST 请求', async ({ request }) => {
    const response = await request.post('https://jsonplaceholder.typicode.com/posts', {
      data: {
        title: '测试标题',
        body: '测试内容',
        userId: 1
      }
    });
    expect(response.status()).toBe(201);
  });
  
  test('结合 UI 和 API', async ({ page, request }) => {
    // 先通过 API 创建测试数据
    const response = await request.post('https://api.example.com/orders', {
      data: { productId: 123, quantity: 1 }
    });
    const order = await response.json();
    
    // 再通过 UI 验证数据展示
    await page.goto(`/orders/${order.id}`);
    await expect(page.locator('.order-status')).toHaveText('已创建');
  });
});

6.5 身份认证管理(StorageState)

每个测试重新登录会导致测试慢且脆弱。使用 storageState 保存登录态,每个测试启动时即登录状态,测试更快、更稳定、可读性更高:

// setup/auth.setup.ts
import { test as setup } from '@playwright/test';

setup('登录并保存状态', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login');
  
  // 等待登录成功
  await page.waitForURL('/dashboard');
  
  // 保存登录状态到文件
  await page.context().storageState({ path: 'auth.json' });
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: {
        // 使用保存的登录状态
        storageState: 'auth.json',
      },
      dependencies: ['setup'],
    },
  ],
});

6.6 视觉回归测试

视觉回归测试需要精细化配置,避免动态区域产生大量无用 diff:

test('视觉回归测试 - 首页', async ({ page }) => {
  await page.goto('/');
  
  // 基础截图对比
  await expect(page).toHaveScreenshot('homepage.png');
  
  // 对特定元素截图
  await expect(page.locator('.header')).toHaveScreenshot('header.png');
  
  // 使用 mask 遮盖动态区域(如时间戳、广告位、头像)
  await expect(page).toHaveScreenshot('homepage-masked.png', {
    mask: [page.locator('.timestamp'), page.locator('.ad-banner')],
  });
  
  // 设置对比阈值
  await expect(page).toHaveScreenshot('homepage.png', {
    threshold: 0.2,  // 允许 20% 的像素差异
  });
});

第六章小结:Playwright 的高级特性——网络拦截、移动模拟、多上下文、API 测试、认证管理和视觉回归,使其不仅能做简单的 UI 自动化,更能胜任复杂的企业级测试需求。

第七章:调试与问题排查

7.1 强大的调试工具

Playwright 提供了一系列调试工具,帮助快速定位问题:

# 1. UI 模式 - 交互式调试
npx playwright test --ui

# 2. 有头模式 - 观察浏览器操作
npx playwright test --headed

# 3. 慢动作模式 - 放慢操作速度
npx playwright test --headed --slowmo=1000

# 4. 调试模式 - 进入 Node.js 调试器
npx playwright test --debug

# 5. 仅运行失败的测试
npx playwright test --only-failed

# 6. 代码生成器 - 录制操作生成脚本
npx playwright codegen https://example.com

7.2 Trace Viewer

Trace Viewer 是 Playwright 最强大的调试工具,可以记录操作视频、网络请求和 DOM 快照,实现分钟级故障定位:

// 在测试中开启 Trace
test('使用 Trace 调试', async ({ page }) => {
  // 开始记录 Trace
  await page.context().tracing.start({ screenshots: true, snapshots: true });
  
  await page.goto('https://example.com');
  await page.click('#button');
  
  // 停止并保存 Trace
  await page.context().tracing.stop({ path: 'trace.zip' });
});

查看 Trace:

npx playwright show-trace trace.zip

按需开启 Trace:仅在测试失败或重试时开启 Trace,保证快速通过,同时失败时有完整信息。

7.3 常见问题与解决方案

问题一:元素找不到

// 解决方案1:确认元素是否在 iframe 中
const frame = page.frameLocator('#my-iframe');
await frame.locator('button').click();

// 解决方案2:使用 waitForSelector 等待元素出现
await page.waitForSelector('.dynamic-element', { state: 'visible' });
await page.locator('.dynamic-element').click();

// 解决方案3:打印页面内容调试
console.log(await page.content());

问题二:点击无效

// 解决方案1:等待元素可点击
const button = page.locator('#submit');
await button.waitFor({ state: 'visible' });
await button.click();

// 解决方案2:使用 JavaScript 强制点击
await page.evaluate(() => {
  document.querySelector('#submit').click();
});

// 解决方案3:使用 force 选项(不推荐,仅作临时方案)
await page.click('#submit', { force: true });

问题三:跨域问题

// 在 playwright.config.ts 中配置
export default defineConfig({
  use: {
    // 忽略 HTTPS 错误
    ignoreHTTPSErrors: true,
    // 设置 baseURL
    baseURL: 'https://example.com',
  },
});

第八章:并行测试与性能优化

Playwright 支持高效并行测试,显著缩短执行时间。通过配置 workers、分片运行、标签筛选及合理隔离策略,可实现快速稳定的自动化测试。

8.1 基础并行配置

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

export default defineConfig({
  // 使用系统 CPU 核心数的 50% 作为默认 workers
  workers: process.env.CI ? 4 : '50%',
  
  // 或者固定 worker 数量
  // workers: 4,
  
  // 完全并行模式
  fullyParallel: true,
  
  use: {
    baseURL: 'https://your-app.com',
    headless: true,
  },
});

实践经验:在 CI/CD 环境中,通常设置为固定值(如 4),因为 CI 环境的核心数是确定的。本地开发可以使用百分比,自动适应不同开发者的机器配置。

8.2 测试隔离

并行测试最大的挑战是测试隔离。确保测试之间没有依赖关系:

// ❌ 错误的示例 - 测试之间存在依赖
test.describe('用户流程', () => {
  test('注册用户', async ({ page }) => {
    // 注册操作
  });
  
  test('登录用户', async ({ page }) => {
    // 这里假设上一步已注册用户 - 这会导致并行执行时失败
  });
});

// ✅ 正确的做法 - 每个测试独立
test.describe('认证模块', () => {
  test('用户注册流程', async ({ page }) => {
    // 完整的注册测试
  });
  
  test('用户登录流程', async ({ page }) => {
    // 使用预置的测试账号,不依赖其他测试
  });
});

使用独立测试数据

test('用户操作测试', async ({ page }) => {
  // 为每个测试生成唯一用户
  const uniqueUser = `testuser_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  
  await page.fill('#username', uniqueUser);
  await page.fill('#password', 'TestPass123!');
  // ... 其他操作
});

8.3 Sharding(分片运行)

当测试套件非常庞大时,单机并行可能仍不够快。这时可以使用分片:

# 将测试分成 4 个分片,在 4 台机器上运行
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4

GitHub Actions 配置示例:

jobs:
  test-shard:
    strategy:
      matrix:
        shard-index: [1, 2, 3, 4]
        shard-total: [4]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npx playwright test --shard=${{ matrix.shard-index }}/${{ matrix.shard-total }}

8.4 基于标签的筛选执行

// 给测试添加标签
test('关键路径测试 @critical', async ({ page }) => {
  // 关键业务逻辑测试
});

test('性能测试 @slow', async ({ page }) => {
  // 耗时较长的性能测试
});

test('UI 细节测试 @visual', async ({ page }) => {
  // 视觉相关测试
});
# 只运行关键测试
npx playwright test --grep "@critical"

# 排除慢测试
npx playwright test --grep-invert "@slow"

8.5 性能优化建议

1. 合理设置并发数:盲目增加并发可能引发资源竞争,反而不快。先 Profile 测试套件,找出瓶颈。只在总耗时确实降低时才增加 worker 数量。

2. Trace 和视频只在必要时开启:全程录制 Trace / 视频浪费时间和存储。仅在测试失败或重试时开启 Trace:

// playwright.config.ts
export default defineConfig({
  use: {
    trace: 'on-first-retry',  // 只在重试时记录
    video: 'retain-on-failure', // 只在失败时保存视频
  },
});

3. 通过 API 准备测试数据:UI 操作慢且容易失败。优先用后端接口准备测试数据,然后在 UI 验证结果。

4. 复用登录态:使用 storageState 保存登录态,避免每个测试重复登录。

第八章小结:并行测试是 Playwright 的核心优势之一。合理配置 workers、做好测试隔离、善用分片和标签筛选,可以让测试执行时间从数小时缩短到几分钟。

第九章:CI/CD 集成

9.1 GitHub Actions 集成

Playwright 官方提供了 GitHub Actions 的完整支持:

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      
      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shard }}/4
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ matrix.shard }}
          path: playwright-report/

9.2 Docker 化执行

Playwright 提供了官方的 Docker 镜像,方便在容器化环境中运行:

FROM mcr.microsoft.com/playwright:v1.43.0-focal

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

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

构建和运行:

docker build -t playwright-tests .
docker run playwright-tests

9.3 生成测试报告

Playwright 内置了 HTML 报告生成功能:

# 运行测试并生成报告
npx playwright test

# 查看报告
npx playwright show-report

集成 Allure 报告:

npm install allure-commandline allure-playwright --save-dev
// playwright.config.ts
export default defineConfig({
  reporter: [
    ['list'],
    ['allure-playwright']
  ],
});

第九章小结:Playwright 与 CI/CD 的集成非常顺畅。官方提供 GitHub Actions 支持和 Docker 镜像,使自动化测试可以轻松融入 DevOps 流程。

第十章:最佳实践与避坑指南

基于行业实践经验,总结 Playwright 最容易踩的坑及解决方案。

10.1 测试组织

按风险级别组织测试:按功能模块组织测试会导致发版流水线臃肿,低风险 UI 也占用时间。

解决方案:

  • 高风险场景(登录、下单、支付)快速精准覆盖并严格断言
  • 低风险 UI 细节放到夜间全量回归
  • 维护 @smoke@full 标签,冒烟测试每次提交跑,全量回归在夜间或发版前跑

10.2 定位策略

优先使用 data-testid,作为代码与测试的契约。在 PR 检查中要求核心 UI 元素必须加测试 ID。

10.3 等待机制

充分利用 Playwright 自动等待和 web-first 断言。必要时绑定到明确信号:网络请求、元素出现或 URL 变化,而非毫秒数。

10.4 数据准备

通过 API 准备测试数据。UI 操作慢且容易失败。若无测试专用接口,可创建受控 /api/test/* 命名空间,仅在 CI 环境开启。

10.5 Mock 外部依赖

第三方接口不稳定导致测试挂。使用 HAR 文件或 stub 关键接口保证稳定性。保留一套真实环境 Canary 测试监控外部接口变化。

10.6 视觉回归

动态区域可能导致大量无用 diff。对动态区域设置 mask 或阈值。从小范围开始(收据、PDF 或核心仪表盘),逐步扩大覆盖面。

10.7 代码组织

传统 Page Object 容易臃肿,难维护。采用“剧本式” helper 函数,用稳定定位器组合业务操作。测试代码读起来像讲故事,更直观易懂。

10.8 不稳定性管理

用注解标记不稳定测试。跟踪每个 spec 文件的不稳定率,超过 1% 就该修复。

10.9 测试报告优化

标准化产物命名,突出关键信息:失败步骤、截图、Trace、网络请求。配置 CI,把 HTML 报告和 Trace 暴露为构建产物。定位问题只需两次点击,不搞寻宝游戏。

10.10 十二大避坑总结

#坑点解决方案
1按功能模块组织测试按风险级别组织,使用标签分层执行
2脆弱的 CSS/文本选择器优先使用 data-testid 和 role
3手写 waitForTimeout使用内置自动等待和 web-first 断言
4每个测试重新登录使用 storageState 保存登录态
5UI 操作准备数据通过 API 准备测试数据
6第三方接口不稳定使用 HAR/Mock,保留 Canary 监控
7视觉回归噪音大使用 mask 和阈值精细化控制
8全程录制 Trace/视频仅在失败或重试时开启
9盲目增加并发先 Profile,找出瓶颈再调整
10Page Object 臃肿采用业务剧本式 helper 函数
11掩盖不稳定测试标记和跟踪不稳定率
12报告难读难定位标准化报告,暴露关键产物

第十一章:Playwright 2025-2026 生态前瞻

11.1 AI 增强测试

Playwright 在 2025 年完成了从“单一测试工具”到“全链路测试平台”的关键跨越。

自然语言驱动测试:通过指令如“测试 iOS Safari 结账流程(4G 网络)”,AI 自动生成脚本并输出带视频的报告。

自愈定位器:当 UI 变更导致元素定位失效时,AI 结合 DOM 快照生成语义化选择器(如 get_by_role("button")),使脚本故障率下降 30%。

混合模式测试:对于高频稳定场景,采用录制的 .test 结构化文件执行,绕过 LLM 推理实现 50ms/操作的高速执行;对于探索性场景,则通过 AI 生成新脚本覆盖边缘案例。

11.2 Playwright MCP

MCP(Model Context Protocol)定义了大型语言模型与外部服务交互的规范。当 Playwright 与 MCP 结合时,创建了对话式自动化的新范式。

2025 年初,某知名电商公司在引入 Playwright MCP 后,UI 自动化测试脚本编写时间从原来的 3 天减少到 2 小时,测试覆盖率提升了 40%,而这一切,测试人员几乎没有编写一行传统脚本。

11.3 企业级部署

  • Docker 化执行:官方提供 Docker 镜像,支持一键部署
  • 云测试平台集成:通过 BrowserStack/Sauce Labs 实现千级并发,测试耗时降低 60%
  • Kubernetes 动态扩缩容:实现测试节点的动态扩缩容,使企业级并发测试资源利用率提升 40%

第十二章:学习路线与资源推荐

12.1 学习路线图

根据 2026 年的最佳实践,建议按以下 10 步系统学习 Playwright:

步骤内容时间
1JavaScript/TypeScript 基础(重点是 async/await)1-2 周
2安装 Playwright,理解项目结构1 天
3编写第一个测试2-3 天
4深入掌握定位器和断言3-5 天
5Page Object Model 设计模式1 周
6API 测试2-3 天
7CI/CD 集成2-3 天
8Trace Viewer 调试1-2 天
9高级模式(Sharding、并行、Mock)1 周
10AI 生态系统(MCP、测试代理)持续学习

12.2 资源推荐

  • 官方文档:https://playwright.dev/ —— 最权威的学习资料
  • 官方 GitHub:https://github.com/microsoft/playwright
  • 社区资源合集:Playwright 优秀资源合集项目,包含大量实战案例和工具

结语

Playwright 作为微软开源的现代 Web 自动化测试框架,凭借其直连浏览器协议的高速通信、智能等待机制、跨浏览器统一 API 以及丰富的企业级特性,正在快速取代 Selenium 成为新一代自动化测试的首选方案。

从入门到企业级实战,我们系统性地学习了 Playwright 的安装配置、元素定位、页面交互、网络拦截、并行测试、CI/CD 集成等核心知识点,并深入探讨了 2025-2026 年 Playwright 与 AI 深度融合的前沿趋势。

那些曾经让测试团队头疼不已的“周五晚上随机失败”,终将成为历史。数据显示,团队从 Selenium 迁移到 Playwright 后,回归时间可缩短 60%,脚本维护成本可下降 70%。更重要的是,测试工程师不再是“代码苦力”,而是化身为指挥智能数字军团的“测试指挥官”。

无论你是刚刚接触自动化测试的新手,还是正在考虑从 Selenium 迁移的资深工程师,希望这篇文章能成为你 Playwright 学习之旅的可靠指南。

开始你的 Playwright 之旅吧,解锁 Web 自动化的无限可能!

更多推荐