首页 Playwright 自动化测试
1.54 unknown microsoft/playwright
打开 Zread CodeWiki
340px

一眼入口

这是什么:Microsoft开发的跨浏览器自动化库,支持Chromium/Firefox/WebKit,提供自动等待、网络拦截、可视化调试。
最短路径:安装浏览器 → 写测试 → 运行 npx playwright test
核心优势:自动等待、强大定位器、跨浏览器、多语言支持

快速安装

# 安装Playwright(含浏览器)
npm init playwright@latest
# 或单独安装
npm install @playwright/test
npx playwright install

# 安装指定浏览器
npx playwright install chromium
npx playwright install --with-deps  # 含系统依赖

最小工作流

起手式

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

test('基础测试', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page.getByRole('heading')).toHaveText('Example');
});

配置文件

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

核心类速查

Browser / BrowserContext / Page

// 启动浏览器
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  viewport: { width: 1280, height: 720 },
  colorScheme: 'dark',
});
const page = await context.newPage();

// 导航
await page.goto('https://example.com', {
  waitUntil: 'domcontentloaded',  // load | domcontentloaded | networkidle
  timeout: 30000,
});

// 清理
await page.close();
await context.close();
await browser.close();
职责隔离级别
Browser浏览器实例整个进程
BrowserContext浏览器上下文(独立Cookie/缓存)context级
Page页面/标签页page级

定位器速查

语义定位器(推荐)

方法用途示例
get_by_role(role, name)ARIA角色get_by_role('button', { name: 'Submit' })
get_by_label(text)表单标签get_by_label('Email').fill('a@b.com')
get_by_placeholder(text)占位符get_by_placeholder('Search...')
get_by_text(text)可见文本get_by_text('Welcome')
get_by_alt_text(text)alt属性get_by_alt_text('logo')
get_by_title(text)title属性get_by_title('Close')
get_by_test_id(id)data-testidget_by_test_id('directions')

CSS / XPath

page.locator('button.submit')           // CSS类
page.locator('#submit-btn')             // CSS ID
page.locator('[data-testid="btn"]')     // 属性
page.locator('xpath=//button[1]')       // XPath
page.locator('//button[text()="Go"]')   // XPath文本

过滤与链式

// 文本过滤
page.get_by_role('listitem').filter(has_text='Product 2')

// 组合定位
page.get_by_role('dialog').filter(
  has=page.get_by_role('heading', { name: 'Confirm' })
)

// nth / first / last
page.get_by_role('listitem').nth(2)
page.get_by_role('listitem').first
page.get_by_role('listitem').last

// iframe定位
page.frame_locator('#iframe-id').get_by_role('button').click()

常用操作速查

点击与输入

// 点击
await page.getByRole('button').click()
await page.locator('.btn').click({ button: 'right', clickCount: 2 })

// 填充(清空后输入)
await page.getByLabel('Email').fill('test@example.com')

// 按键输入(逐字符,有延迟)
await page.getByPlaceholder('Search').type('query', { delay: 50 })

// 特殊键
await page.locator('input').press('Enter')
await page.locator('input').press('Control+A')
await page.locator('input').press('Tab')

表单操作

// 下拉选择
await page.locator('select').selectOption('value')
await page.getByRole('combobox').select_option(['val1', 'val2'])

// 复选框
await page.get_by_role('checkbox', { name: 'Remember me' }).check()

// 清空
await page.get_byLabel('Input').clear()

文件与对话框

// 文件上传
await page.setInputFiles('input[type=file]', 'path/to/file.png')
await page.setInputFiles('input[type=file]', [])  // 清空

// 对话框处理
page.on('dialog', dialog => dialog.accept('answer'))
page.on('dialog', dialog => dialog.dismiss())

// 文件下载
const [download] = await Promise.all([
  page.waitForEvent('download'),
  page.getByRole('button', { name: 'Download' }).click(),
]);
await download.saveAs('/path/to/file');

等待策略速查

策略用法场景
自动等待await page.getByRole('button').click()默认,元素就绪自动等待
显式等待await page.waitForSelector('.loaded', { timeout: 5000 })特定条件
网络等待await page.waitForLoadState('networkidle')API调用后
函数等待await page.waitForFunction(() => window.status === 'ready')条件判断
响应等待await page.waitForResponse(url => url.includes('/api'))拦截响应
// expect系列
await expect(page.getByRole('heading')).toHaveText('Welcome')
await expect(page.getByLabel('Email')).toHaveValue('test@example.com')
await expect(page.locator('.loading')).toBeHidden()

// 软断言(不中断测试)
await expect.soft(page.getByRole('heading')).toHaveText('Welcome')

截图与PDF

// 普通截图
await page.screenshot({ path: 'screenshot.png' })

// 全页截图
await page.screenshot({ path: 'full.png', fullPage: true })

// 元素截图
await page.locator('.header').screenshot({ path: 'header.png' })

// 截图对比(视觉回归)
await expect(page).toHaveScreenshot('home.png', {
  maxDiffPixels: 100,
  maxDiffPixelRatio: 0.2,
})

// PDF(仅Chromium)
await page.emulate_media({ media: 'screen' })
await page.pdf({
  path: 'output.pdf',
  format: 'A4',
  printBackground: true,
  margin: { top: '1cm', bottom: '1cm' },
})

网络拦截速查

// 拦截请求
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());

// 修改响应
await page.route('**/api/**', async route => {
  if (route.request().url().includes('/api/data')) {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ mock: true }),
    });
  } else {
    await route.continue();
  }
});

// 等待请求/响应
const [response] = await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/api') && resp.status() === 200),
  page.getByText('Submit').click(),
]);

CLI命令速查

命令说明常用选项
npx playwright test运行测试--headed, --debug, -g <grep>, --project
npx playwright install安装浏览器--with-deps, --force, chromium
npx playwright show-report显示报告--port 8080
npx playwright codegen [url]生成测试代码--target=python, --device="iPhone 13"
npx playwright open [url]快速打开页面--browser=chromium, --device
npx playwright screenshot <url> <file>页面截图--full-page
npx playwright show-trace <file>查看跟踪--port 8080

调试模式

# 启动调试器
npx playwright test --debug

# 浏览器控制台访问playwright对象
PWDEBUG=console npx playwright test

# 在测试中暂停
await page.pause();

Trace Viewer

// 配置跟踪录制
export default defineConfig({
  use: { trace: 'on-first-retry' },
});

// 手动控制
await page.tracing.start({ screenshots: true, snapshots: true });
// ... 执行操作 ...
await page.tracing.stop();
# 查看跟踪文件
npx playwright show-trace trace.zip

高频场景Recipes

场景1:表单提交测试

test('登录表单提交', async ({ page }) => {
  await page.goto('/login');

  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Sign In' }).click();

  await expect(page).toHaveURL('**/dashboard');
  await expect(page.getByRole('heading')).toHaveText('Welcome');
});

场景2:数据抓取

test('抓取产品列表', async ({ page }) => {
  await page.goto('/products');

  const products = await page.locator('.product-item').evaluateAll(
    items => items.map(item => ({
      title: item.querySelector('h3')?.textContent,
      price: item.querySelector('.price')?.textContent,
    }))
  );

  console.log(products);
});

场景3:跨浏览器配置

export default defineConfig({
  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'] } },
  ],
});

场景4:代理与认证状态

// 代理配置
const context = await browser.newContext({
  proxy: {
    server: 'http://proxy:3128',
    username: 'user',
    password: 'pass',
  },
});

// 保存/加载认证状态
await page.context().storageState({ path: 'auth.json' });
const context2 = await browser.newContext({
  storageState: './auth.json',
});

Page Object模式

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.getByLabel('Email').fill(email);
    await this.page.getByLabel('Password').fill(password);
    await this.page.getByRole('button', { name: 'Sign In' }).click();
  }

  async expectError() {
    await expect(this.page.locator('.error')).toBeVisible();
  }
}

// tests/login.spec.ts
test('登录失败', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('bad@example.com', 'wrong');
  await loginPage.expectError();
});

Fixtures与Hooks

// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

export test = base.extend({
  loginPage: async ({ page }, use) => {
    const lp = new LoginPage(page);
    await lp.goto();
    await use(lp);
  },

  authenticatedPage: async ({ browser }, use) => {
    const ctx = await browser.newContext();
    const page = await ctx.newPage();
    // 执行登录...
    await use(page);
    await ctx.close();
  },
});

// 使用
test('用户资料', async ({ authenticatedPage }) => {
  // 已登录状态
});

测试分类与命名

// 按标签分类
test.describe('Login', () => {
  test('验证邮箱格式 @slow', async ({ page }) => {});
  test('密码错误提示 @smoke', async ({ page }) => {});
});

// 运行特定标签
// npx playwright test --grep @smoke
// npx playwright test --grep-invert @slow

常见坑与风险点

原因解决方案
固定延迟waitForTimeout 不稳定使用自动等待或 waitForSelector
networkidle不可靠现代SPA持续请求使用 waitForLoadState('load')
iframe操作失败未使用frameLocatorpage.frameLocator('#iframe').locator(...)
上下文共享测试间状态污染每个测试用独立context
截图不一致视口/字体差异配置统一viewport和使用字体

Quick Ref

// 最小启动
const { chromium } = require('playwright');
(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await browser.close();
})();

// 常用配置速记
timeout: 30000          // 默认超时
waitUntil: 'load'       // 导航等待
headless: true          // 无头模式
fullPage: true          // 全页截图

参考资源

  • 官方文档:https://playwright.dev
  • API参考:https://playwright.dev/docs/api/class-page
  • 配置指南:https://playwright.dev/docs/test-configuration
  • 最佳实践:https://playwright.dev/docs/best-practices
  • Trace Viewer:https://playwright.dev/docs/trace-viewer