Playwright 从零到 CI 实战:基于 TypeScript 的端到端测试完整指南
本文基于一个可运行的
playwright-demo项目,从环境搭建、配置拆解、用例编写、定位器与断言、调试排障,一路讲到 GitHub Actions 持续集成。适合有一定前端/测试基础、想系统掌握 Playwright 的同学。
一、为什么选择 Playwright?
Playwright 是微软开源的现代化端到端(E2E)测试框架,与 Selenium、Cypress 相比,它在工程化体验上有几个突出优势:
| 能力 | 说明 |
|---|---|
| 多浏览器同源 API | 一套 API 覆盖 Chromium、Firefox、WebKit,真正做到跨引擎一致 |
| 自动等待(Auto-waiting) | 操作前自动等待元素可交互,大幅减少 sleep / 脆弱的显式等待 |
| 隔离性强 | 每个测试默认使用独立 Browser Context,状态不互相污染 |
| 开箱即用的工具链 | Codegen、UI Mode、Trace Viewer、HTML Report 一体 |
| 对现代前端友好 | 原生支持 SPA、Shadow DOM、多页签、文件下载/上传等场景 |
| TypeScript 一等公民 | 官方脚手架默认 TS,类型提示完善 |
简单理解:Playwright = 浏览器自动化引擎 + 测试运行器 + 调试/报告工具链。
本项目使用:
@playwright/test:^1.62.0@types/node:^26.1.1- 测试目录:
e2e/ - 配置文件:
playwright.config.ts - CI:GitHub Actions(
.github/workflows/playwright.yml)
二、环境准备与项目初始化
2.1 前置条件
- Node.js(建议 LTS,例如 18/20/22)
- npm / pnpm / yarn 任一包管理器
- 能访问外网(首次需要下载浏览器二进制)
2.2 初始化方式一:官方脚手架(推荐)
bash
npm init playwright@latest
交互过程中通常会问:
- 使用 TypeScript 还是 JavaScript?
- 测试目录叫什么(本项目为
e2e)? - 是否添加 GitHub Actions 工作流?
- 是否立即安装浏览器?
2.3 初始化方式二:在已有项目中手动安装
bash
npm init -y
npm install -D @playwright/test @types/node
npx playwright install
说明:
npm install -D @playwright/test:安装测试框架(含断言、fixture、CLI)npx playwright install:下载 Chromium / Firefox / WebKit 浏览器内核- CI 环境常用:
npx playwright install --with-deps(同时安装系统依赖,尤其是 Linux)
本仓库 package.json 核心依赖如下:
json
{
"name": "playwright-demo",
"version": "1.0.0",
"type": "commonjs",
"devDependencies": {
"@playwright/test": "^1.62.0",
"@types/node": "^26.1.1"
}
}
注意:Playwright 测试文件可以是
.ts,即使package.json里"type": "commonjs",Playwright 测试运行器也会自行处理 TypeScript 转译,无需你再单独配ts-node。
三、项目结构拆解
一个典型 Playwright 项目大致如下:
text
playwright-demo/
├── e2e/ # 正式测试目录(testDir)
│ └── example.spec.ts # 示例用例
├── tests/ # 脚手架可能残留的旧目录(本项目配置未指向它)
│ └── example.spec.ts
├── playwright.config.ts # 核心配置
├── package.json
├── package-lock.json
├── README.md
├── .gitignore
├── .github/workflows/
│ └── playwright.yml # CI 流水线
├── playwright-report/ # HTML 报告输出(运行后生成,通常 gitignore)
└── test-results/ # 失败产物:截图、trace、error-context(gitignore)
3.1 为什么有 e2e/ 和 tests/ 两个目录?
本项目 playwright.config.ts 中明确写了:
ts
testDir: './e2e',
因此 真正会被执行的是 e2e/ 。tests/ 若存在,只是历史残留或对照样例,不会被默认扫描。
建议:团队统一只保留一个测试根目录,避免"改了 tests,CI 却跑 e2e"的困惑。
3.2 .gitignore 该忽略什么?
gitignore
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
| 目录 | 作用 |
|---|---|
test-results/ |
失败时的截图、视频、trace、error-context |
playwright-report/ |
npx playwright show-report 用的 HTML 报告 |
blob-report/ |
分布式/分片合并报告时的中间产物 |
.auth/ |
常用于存储登录态(storageState),勿提交敏感信息 |
四、核心配置详解:playwright.config.ts
配置是 Playwright 工程化的灵魂。下面按本项目真实配置逐项讲解。
ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 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'] } },
],
// webServer: { ... }
});
4.1 testDir
指定测试文件根目录。默认会匹配 **/*.(spec|test).(js|ts|mjs)。
4.2 fullyParallel
true:同一文件内的多个test()也可并行false:文件内串行,文件间仍可并行(取决于 workers)
适合:用例互相独立、无共享状态 的场景。若用例依赖"上一条创建的数据",不要开这个,或改用 test.describe.serial。
4.3 forbidOnly
ts
forbidOnly: !!process.env.CI,
本地调试时你可能会写 test.only(...)。若把 only 提交到仓库,CI 会只跑这一条,其他全跳过------非常危险。
开启后:CI 环境一旦发现 test.only / describe.only,直接失败,强制你改回来。
4.4 retries 与 workers(本地 vs CI 差异)
ts
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
| 配置 | 本地 | CI | 原因 |
|---|---|---|---|
retries |
0 | 2 | CI 偶发抖动(网络、机器负载)更常见,失败可重试;本地失败应立刻暴露 |
workers |
自动(通常按 CPU) | 1 | CI 资源有限,串行更稳;本地并行更快 |
重试不是万能药。若用例本身不稳定(竞态、硬编码等待),应优先修用例,而不是靠 retries 掩盖。
4.5 reporter
ts
reporter: 'html',
运行结束后生成 HTML 报告。常用组合:
ts
reporter: [
['list'],
['html', { open: 'never' }],
['junit', { outputFile: 'results.xml' }], // 给 Jenkins / 质量门禁
],
查看报告:
bash
npx playwright show-report
4.6 use:所有用例共享的浏览器选项
ts
use: {
// baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
baseURL
设置后,page.goto('/login') 会自动拼成 http://localhost:3000/login,便于环境切换(dev/staging)。
trace
Trace 是 Playwright 最强调试手段之一,会记录:
- DOM 快照
- 网络请求
- 控制台日志
- 每一步 action 的前后状态
常用取值:
| 值 | 含义 |
|---|---|
off |
不记录 |
on |
每次都记(体积大) |
retain-on-failure |
失败保留 |
on-first-retry |
仅第一次重试时记录(本项目配置,兼顾体积与可观测性) |
查看:
bash
npx playwright show-trace trace.zip
4.7 projects:多浏览器矩阵
ts
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
]
devices[...] 会预设视口、User-Agent、是否 touch 等。一条用例会被跑 3 遍(三浏览器)。
只跑某一浏览器:
bash
npx playwright test --project=chromium
还可扩展移动端 / 品牌浏览器(配置里已有注释模板):
ts
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
{
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
4.8 webServer(测本地应用时强烈建议开启)
本项目测的是公网 https://playwright.dev/,所以没开。若测自己的前端:
ts
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
含义:
- 测试开始前自动启动本地服务
- 等到
url可访问再跑用例 - 本地可复用已启动的服务;CI 强制重新拉起,保证干净环境
4.9 环境变量与 dotenv(可选)
配置顶部预留了 dotenv 写法:
ts
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
适合把账号、baseURL、密钥放到 .env(切记加入 .gitignore)。
五、第一条用例:逐行精读
本项目 e2e/example.spec.ts:
ts
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
5.1 test 与 Fixture 注入
ts
test('has title', async ({ page }) => { ... })
{ page } 不是普通参数,而是 Playwright Fixture。框架会为每个测试:
- 创建 Browser(或复用)
- 创建独立 BrowserContext
- 创建 Page
- 测试结束后自动关闭,保证隔离
常见内置 fixture:page、context、browser、request(API 测试)、baseURL 等。
5.2 page.goto
ts
await page.goto('https://playwright.dev/');
默认等待事件是 load。可选:
ts
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.goto(url, { waitUntil: 'networkidle' }); // SPA 慎用,可能一直等
await page.goto(url, { timeout: 60_000 });
5.3 断言:expect 是 Web-First 的
ts
await expect(page).toHaveTitle(/Playwright/);
注意这里的 await:Playwright 的断言会自动重试,直到超时或条件满足。这与普通 Jest 同步断言不同。
正则 /Playwright/ 表示标题"包含"即可,不必全等。
5.4 定位器:优先用角色定位(Role-based)
ts
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
这是官方推荐的 Accessibility Tree(无障碍树)定位:
- 更贴近真实用户感知
- 对 CSS 类名、DOM 结构变动更不敏感
- 顺带倒逼产品可访问性更好
定位器优先级建议(从高到低)
getByRole/getByLabel/getByPlaceholder/getByTextgetByTestId(专门为测试加的data-testid)- CSS / XPath(最后手段)
示例对比:
ts
// ✅ 推荐
page.getByRole('button', { name: '提交' })
// ✅ 测试专用属性
page.getByTestId('submit-btn')
// ⚠️ 脆弱:样式类一变就挂
page.locator('.btn-primary.ml-2')
5.5 自动等待到底等什么?
对 Locator 执行 click() 时,Playwright 会等待元素:
- 出现在 DOM
- 可见
- 稳定(不在动画中乱跳)
- 可接收事件(不被遮挡)
- 已启用(非 disabled)
因此多数场景 不需要 waitForTimeout(3000)。硬编码 sleep 通常是坏味道。
六、常用命令速查
bash
# 跑全部测试(按 projects 矩阵展开)
npx playwright test
# UI 模式:可视化挑选用例、看时间线、定位器探索
npx playwright test --ui
# 只跑 Chromium
npx playwright test --project=chromium
# 按文件名过滤
npx playwright test example
# 调试模式(步进、Inspector)
npx playwright test --debug
# 录制脚本(Codegen)
npx playwright codegen https://playwright.dev/
# 看 HTML 报告
npx playwright show-report
# 有头模式(看见浏览器)
npx playwright test --headed
# 更新快照(若使用 toHaveScreenshot / toMatchSnapshot)
npx playwright test --update-snapshots
6.1 Codegen 怎么用?
bash
npx playwright codegen https://playwright.dev/
会打开浏览器 + Playwright Inspector。你在页面上点击、输入,右侧自动生成定位器代码。适合:
- 快速摸清页面结构
- 生成初稿后再人工改成更稳的 Role/TestId 定位
不要把 Codegen 产物原样当最终代码------它有时会生成偏脆的选择器。
6.2 UI Mode 适合做什么?
npx playwright test --ui 适合:
- 本地开发时反复跑单条用例
- 看每一步的 DOM / 网络
- 用 pick locator 验证定位是否唯一
比纯 --debug 更适合日常迭代。
七、进阶技术点(写真实业务必会)
下面这些在官方 demo 里未必直接出现,但落地项目几乎都会用到。
7.1 自定义 Fixture(登录态复用)
ts
// e2e/fixtures.ts
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
loggedInPage: import('@playwright/test').Page;
};
export const test = base.extend<MyFixtures>({
loggedInPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('用户名').fill('demo');
await page.getByLabel('密码').fill('secret');
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText('工作台')).toBeVisible();
await use(page);
},
});
export { expect };
用例中:
ts
import { test, expect } from './fixtures';
test('进入订单页', async ({ loggedInPage: page }) => {
await page.goto('/orders');
await expect(page.getByRole('heading', { name: '订单列表' })).toBeVisible();
});
7.2 storageState:一次登录,多处复用
ts
// e2e/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('https://example.com/login');
// ... 登录步骤
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
配置:
ts
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
这样业务用例不再重复登录,速度更快、更稳。
7.3 Page Object Model(POM)
当页面交互变复杂时,把定位与操作封装起来:
ts
// e2e/pages/HomePage.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class HomePage {
readonly getStarted: Locator;
constructor(private readonly page: Page) {
this.getStarted = page.getByRole('link', { name: 'Get started' });
}
async goto() {
await this.page.goto('https://playwright.dev/');
}
async openInstallation() {
await this.getStarted.click();
await expect(this.page.getByRole('heading', { name: 'Installation' })).toBeVisible();
}
}
用例变薄:
ts
test('get started link', async ({ page }) => {
const home = new HomePage(page);
await home.goto();
await home.openInstallation();
});
POM 的价值不是"多写类",而是 定位器集中维护、用例可读性提升。
7.4 API 与 UI 混合测试
ts
test('先用 API 造数,再用 UI 验证', async ({ page, request }) => {
const res = await request.post('/api/orders', {
data: { sku: 'A001', qty: 1 },
});
expect(res.ok()).toBeTruthy();
const { id } = await res.json();
await page.goto(`/orders/${id}`);
await expect(page.getByText('A001')).toBeVisible();
});
UI 负责验证交互与展示;数据准备尽量走 API,减少 UI 前置步骤的脆弱性。
7.5 网络拦截与 Mock
ts
await page.route('**/api/user', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Mock User' }),
});
});
适用:后端未就绪、第三方不稳定、要测异常态(500 / 超时)。
7.6 截图与视觉对比
ts
await expect(page).toHaveScreenshot('home.png');
await expect(page.locator('.hero')).toHaveScreenshot('hero.png');
首次运行会生成基线图;之后对比像素差异。CI 中注意字体/抗锯齿导致的跨平台差异,可固定 viewport、用同一 OS 跑视觉测试。
7.7 软断言
ts
await expect.soft(page.getByText('A')).toBeVisible();
await expect.soft(page.getByText('B')).toBeVisible();
// 继续执行,最后统一失败
适合收集同一页面上的多个校验点,避免"挂在第一条就中断"。
八、失败排障实战(结合本项目现象)
本项目跑外网用例时,可能出现类似错误:
text
Test timeout of 30000ms exceeded.
Error: page.goto: Test timeout of 30000ms exceeded.
Call log:
- navigating to "https://playwright.dev/", waiting until "load"
8.1 这是什么意思?
默认单测超时 30s。page.goto 一直等不到 load(网络慢、被墙、DNS、代理、目标站抖动),最终超时失败。
8.2 排查路径
- 本机浏览器能否打开目标站?
- 是否需要代理?(公司网络常见)
- 是否只在某个浏览器失败? (对比
--project=chromium) - 看
test-results/**/error-context.md:Playwright 会保存失败时的可访问性树快照,帮助判断"页面是否其实已经打开了一部分" - 开 Trace / 有头模式复现
8.3 常见缓解手段(按推荐顺序)
ts
// 1)提高单测超时(治标)
test.setTimeout(60_000);
// 2)goto 单独加超时,并放宽等待条件
await page.goto(url, {
timeout: 60_000,
waitUntil: 'domcontentloaded',
});
// 3)配置层统一调整
// playwright.config.ts
export default defineConfig({
timeout: 60_000,
use: {
navigationTimeout: 60_000,
actionTimeout: 15_000,
},
});
更稳妥的工程做法:
- 测自己的应用,用
webServer+baseURL,不要依赖公网 - CI 里对外部依赖做健康检查,或改为 Mock
- 对已知不稳定步骤做有限 retries,并监控 flaky 率
8.4 HTML Report / Trace 怎么读?
失败后:
bash
npx playwright show-report
报告里可以看到:
- 哪个 project(chromium/firefox/webkit)失败
- 失败步骤与调用栈
- 附件:截图、视频、trace、error context
有 trace 时:
bash
npx playwright show-trace path/to/trace.zip
在时间轴上点每一步,看当时 DOM 与网络,比"猜"高效一个数量级。
九、GitHub Actions CI 完整拆解
本项目工作流:.github/workflows/playwright.yml
yaml
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
9.1 触发时机
push到main/master- 针对
main/master的pull_request
保证主干与合入前都有 E2E 门禁。
9.2 为什么用 npm ci 而不是 npm install?
npm ci:
- 严格按
package-lock.json安装 - 先清空
node_modules再装 - 更适合 CI 的可复现构建
9.3 --with-deps 为什么重要?
Linux Runner 上跑浏览器需要系统级依赖(字体、图形库等)。
npx playwright install --with-deps = 装浏览器 + 装系统依赖,少一步就容易出现"本地能跑、CI 起不来浏览器"。
9.4 报告产物上传
yaml
if: ${{ !cancelled() }}
即使测试失败(job failed),只要没被取消,也会上传 playwright-report/,方便在 Actions 页面下载 HTML 报告排查。
retention-days: 30 控制产物保留时长,避免 Artifact 无限堆积。
9.5 与 playwright.config.ts 的联动
CI 通常会设置环境变量 CI=true(GitHub Actions 默认就有)。于是配置中的:
ts
forbidOnly: !!process.env.CI, // true
retries: process.env.CI ? 2 : 0, // 2
workers: process.env.CI ? 1 : undefined, // 1
自动切换到"更严格、更稳"的 CI 模式。
9.6 可继续增强的 CI 技巧
- 缓存浏览器 :用
actions/cache缓存~/.cache/ms-playwright,加速安装 - 分片并行(Sharding):
bash
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
- 只在关键路径变更时跑 E2E (
paths/paths-filter) - 合并报告 :使用 blob reporter +
npx playwright merge-reports
十、最佳实践清单(可直接当团队规范)
- 定位器 :
getByRole/getByLabel/getByTestId优先,少写 CSS/XPath - 等待 :相信 Auto-waiting;禁止随意
waitForTimeout - 隔离:一测一 context;不要依赖执行顺序
- 稳定性:数据准备走 API;UI 只验证必要路径
- 登录 :
setup project + storageState,不要每条用例都点登录 - 超时 :区分
test timeout/navigationTimeout/actionTimeout,按场景设置 - CI :
forbidOnly、有限retries、上传 report/trace - 密钥 :账号密码放环境变量 / Secret,勿提交
.auth到公开仓库 - 目录 :单一
testDir,POM / fixtures / 工具函数分层清晰 - 失败即资产:把 flaky 用例登记治理,而不是无限加大 retries
十一、动手实验建议(跟着本仓库练)
- 安装依赖与浏览器:
bash
npm ci
npx playwright install
- 只跑 Chromium,确认环境通:
bash
npx playwright test --project=chromium
- 打开 UI Mode 改一条断言,观察失败报告:
bash
npx playwright test --ui
- 用 Codegen 录一条新流程,整理成 Role 定位后放进
e2e/ - 给配置加上
baseURL+webServer,改成测自己的本地应用 - Push 到 GitHub,观察 Actions 是否产出可下载的 HTML 报告
十二、总结
Playwright 的学习曲线可以拆成三层:
- 会跑 :安装、
npx playwright test、看报告 - 会写:Locator、Web-First 断言、Fixture、POM
- 会工程化:config 分环境、trace/重试策略、登录态复用、CI Artifact、flaky 治理
本仓库已经覆盖了第 1 层和第 3 层的骨架(多浏览器 projects、HTML reporter、GitHub Actions)。你要做的是把 e2e/example.spec.ts 从"官方演示站冒烟",演进成"你们业务关键路径的回归套件"。
如果你接下来要扩展,优先顺序建议:
baseURL+webServer切到真实被测系统auth.setup.ts+storageState解决登录- 关键路径 POM 化
- CI 缓存与分片提速
- 引入 API 造数与必要 Mock
参考资料
- 官方文档:https://playwright.dev/docs/intro
- 最佳实践:https://playwright.dev/docs/best-practices
- Locators:https://playwright.dev/docs/locators
- Trace Viewer:https://playwright.dev/docs/trace-viewer
- CI 指南:https://playwright.dev/docs/ci
本文示例代码基于 @playwright/test ^1.62.0 与仓库内 playwright.config.ts / e2e/example.spec.ts / .github/workflows/playwright.yml 整理。版本升级时请以官方文档为准。