一、它们是什么?
1.1 Puppeteer
Puppeteer 是由 Google Chrome 团队开发和维护的 Node.js 库,它通过 Chrome DevTools Protocol (CDP) 提供了一套高级 API,用于控制 Chromium/Chrome 浏览器。你可以把它理解为"用代码操控浏览器的手"。
核心特点:
- 由 Google 官方维护,与 Chrome 深度集成
- 默认运行 Headless(无头)模式,无需打开浏览器窗口
- 原生支持 Chrome/Chromium,v23+ 实验性支持 Firefox
- 轻量、启动快、API 简洁
1.2 Playwright
Playwright 是由 Microsoft 开发的跨浏览器自动化框架,最初由 Puppeteer 的原班人马跳槽到微软后打造。它通过各浏览器原生的调试协议,统一控制 Chromium、Firefox、WebKit(Safari 内核) 三大引擎。
核心特点:
- 真正的跨浏览器支持(Chrome、Firefox、Safari 一套代码)
- 内置 自动等待(Auto-Waiting) 机制,大幅减少
sleep/ 显式等待 - 原生支持多页面、多标签页、多浏览器上下文
- 提供 Playwright Test 完整测试框架
- 内置 Codegen(代码生成器) 、Trace Viewer 、Inspector 等调试利器
- 支持 JavaScript/TypeScript、Python、Java、.NET 多语言
1.3 核心对比一览
| 维度 | Puppeteer | Playwright |
|---|---|---|
| 维护方 | Microsoft | |
| 浏览器支持 | Chrome/Chromium(Firefox 实验性) | Chromium + Firefox + WebKit |
| 自动等待 | ❌ 需手动处理 | ✅ 内置智能等待 |
| 多语言 | 仅 JS/TS | JS/TS、Python、Java、.NET |
| 移动端模拟 | 基础支持 | 完善的设备模拟 |
| 网络拦截 | ✅ | ✅(更强大的路由能力) |
| 测试框架 | 无内置,需搭配 Jest/Mocha | 内置 @playwright/test |
| 调试工具 | DevTools | Trace Viewer + Inspector + Codegen |
| 并发模型 | 单浏览器多页面 | 多浏览器上下文,天然隔离 |
| 社区生态 | 成熟稳定 | 增长迅猛,功能迭代快 |
选择建议: 如果你只需要操控 Chrome 做轻量任务(截图、爬虫、PDF),Puppeteer 足够且更轻;如果你需要跨浏览器测试、企业级 E2E 测试、或希望更少的样板代码,Playwright 是更优选择。
二、Puppeteer 完整安装指南
2.1 环境前置要求
| 项目 | 最低要求 | 推荐版本 |
|---|---|---|
| Node.js | >= 18 | 20 LTS 或 22 LTS |
| npm | >= 9 | 10+ |
| 操作系统 | Windows 10+ / macOS 11+ / Ubuntu 20.04+ | 最新稳定版 |
| 磁盘空间 | >= 500MB(含 Chromium) | >= 1GB |
| 内存 | >= 2GB | >= 4GB |
检查命令:
bash
# 检查 Node.js 版本
node -v
# 期望输出: v20.x.x 或 v22.x.x
# 检查 npm 版本
npm -v
# 期望输出: 10.x.x
# 检查 npx 是否可用
npx --version
# 如果版本过低,使用 nvm 升级
# macOS / Linux:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
# Windows: 下载 nvm-windows
# https://github.com/coreybutler/nvm-windows/releases
nvm install 22
nvm use 22
2.2 安装方式一:标准安装(自动下载 Chromium)
适用场景:本地开发、学习、个人项目。安装后开箱即用。
bash
# Step 1: 创建项目目录
mkdir puppeteer-project
cd puppeteer-project
# Step 2: 初始化 npm 项目
npm init -y
# Step 3: 安装 Puppeteer(自动下载匹配的 Chromium,约 170MB)
npm install puppeteer
# Step 4: 验证安装
node -e "console.log(require('puppeteer/package.json').version)"
安装过程说明:
npm install puppeteer会触发postinstall脚本- 该脚本自动从 Google CDN 下载与当前版本匹配的 Chromium
- 下载位置:
node_modules/puppeteer/.local-chromium/(旧版)或~/.cache/puppeteer/(新版)
2.3 安装方式二:puppeteer-core(不下载浏览器)
适用场景:CI/CD 环境、Docker 容器、已有 Chrome 的服务器、体积敏感场景。
bash
# Step 1: 创建项目
mkdir puppeteer-core-project
cd puppeteer-core-project
npm init -y
# Step 2: 安装(不下载任何浏览器,仅 ~5MB)
npm install puppeteer-core
# Step 3: 验证
node -e "console.log(require('puppeteer-core/package.json').version)"
⚠️ 重要区别:
puppeteer:包含浏览器下载逻辑,require('puppeteer')后直接launch()即可puppeteer-core:不包含浏览器,必须在launch()时指定executablePath
2.4 安装方式三:跳过浏览器下载,使用系统 Chrome
ini
# 设置环境变量跳过下载
export PUPPETEER_SKIP_DOWNLOAD=true
npm install puppeteer
# 或者写入 .npmrc 文件(项目级永久生效)
echo "PUPPETEER_SKIP_DOWNLOAD=true" >> .npmrc
npm install puppeteer
代码中指定系统 Chrome 路径:
php
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', // macOS
// executablePath: 'C:\Program Files\Google\Chrome\Application\chrome.exe', // Windows
// executablePath: '/usr/bin/google-chrome-stable', // Linux
});
各系统查找 Chrome 路径:
bash
# macOS
ls "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
# Linux
which google-chrome-stable || which chromium-browser || which chromium
# Windows PowerShell
Get-Command chrome | Select-Object -ExpandProperty Source
# 或
(Get-Item "C:\Program Files\Google\Chrome\Application\chrome.exe").FullName
2.5 国内镜像加速配置
bash
# ===== 方法一:临时环境变量(当前终端有效) =====
export PUPPETEER_DOWNLOAD_BASE_URL=https://npmmirror.com/mirrors/chrome-for-testing
npm install puppeteer
# ===== 方法二:项目级 .npmrc(推荐) =====
echo "PUPPETEER_DOWNLOAD_BASE_URL=https://npmmirror.com/mirrors/chrome-for-testing" >> .npmrc
npm install puppeteer
# ===== 方法三:全局 .npmrc =====
npm config set PUPPETEER_DOWNLOAD_BASE_URL https://npmmirror.com/mirrors/chrome-for-testing
# ===== 方法四:npm 命令参数 =====
PUPPETEER_DOWNLOAD_BASE_URL=https://npmmirror.com/mirrors/chrome-for-testing npm install puppeteer
2.6 Linux 服务器额外依赖安装
Puppeteer 的 Chromium 在 Linux 上需要以下系统库:
csharp
# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y \
ca-certificates \
fonts-liberation \
libasound2 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libcups2 \
libdbus-1-3 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libxshmfence1 \
xdg-utils \
wget
# CentOS / RHEL / Fedora
sudo yum install -y \
alsa-lib \
atk \
at-spi2-atk \
cups-libs \
gtk3 \
libXcomposite \
libXdamage \
libXrandr \
mesa-libgbm \
nss \
pango \
xorg-x11-fonts-100dpi \
xorg-x11-fonts-75dpi \
xorg-x11-fonts-cyrillic \
xorg-x11-fonts-misc \
xorg-x11-fonts-Type1 \
xorg-x11-utils
# Alpine Linux (Docker)
apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont
2.7 安装验证(完整测试脚本)
javascript
# 创建验证脚本
cat > verify-puppeteer.js << 'EOF'
const puppeteer = require('puppeteer');
(async () => {
try {
console.log('🚀 正在启动浏览器...');
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
console.log('📄 正在打开页面...');
const page = await browser.newPage();
await page.goto('https://example.com', { timeout: 30000 });
const title = await page.title();
console.log('✅ 页面标题:', title);
await page.screenshot({ path: 'verify-screenshot.png' });
console.log('✅ 截图已保存: verify-screenshot.png');
await browser.close();
console.log('🎉 Puppeteer 安装验证全部通过!');
} catch (error) {
console.error('❌ 验证失败:', error.message);
process.exit(1);
}
})();
EOF
# 运行验证
node verify-puppeteer.js
2.8 Puppeteer 安装注意事项
| 序号 | 注意事项 | 说明 |
|---|---|---|
| 1 | 不要使用 sudo npm install |
会导致权限混乱,应修复 npm 全局目录权限或使用 nvm |
| 2 | Chromium 下载失败时先检查网络 | 公司内网需配置代理:export HTTPS_PROXY=http://proxy:8080 |
| 3 | puppeteer 与 puppeteer-core 不要混装 | 二者功能重叠,选一个即可 |
| 4 | 版本锁定 | 生产环境建议 npm install puppeteer@23.x.x --save-exact |
| 5 | Docker 中必须加 --no-sandbox |
否则 Chromium 无法启动 |
| 6 | 中文字体缺失 | Linux 服务器需安装 fonts-noto-cjk |
| 7 | 磁盘空间 | 每次升级 Puppeteer 会下载新版 Chromium,注意清理旧版本 |
| 8 | Node.js 版本兼容 | Puppeteer v23+ 要求 Node >= 18,旧版可能要求不同 |
2.9 Puppeteer 卸载与清理
bash
# 卸载包
npm uninstall puppeteer
# 清理下载的 Chromium 缓存
rm -rf ~/.cache/puppeteer
# 或(旧版路径)
rm -rf node_modules/puppeteer/.local-chromium
# 完全清理项目
rm -rf node_modules package-lock.json
三、Playwright 完整安装指南
3.1 环境前置要求
| 项目 | 最低要求 | 推荐版本 |
|---|---|---|
| Node.js | >= 18 | 20 LTS 或 22 LTS |
| npm | >= 9 | 10+ |
| Python(可选) | >= 3.8 | 3.11+ |
| 操作系统 | Windows 10+ / macOS 11+ / Ubuntu 20.04+ | 最新稳定版 |
| 磁盘空间 | >= 1.5GB(三大浏览器) | >= 3GB |
| 内存 | >= 4GB | >= 8GB |
3.2 安装方式一:官方初始化向导(最推荐)
适用场景:新建 E2E 测试项目。一条命令搞定所有配置。
ini
# Step 1: 在目标项目根目录运行(或新建目录)
npm init playwright@latest
# ===== 交互式问答(推荐选项) =====
# ? Do you want to use TypeScript or JavaScript?
# → TypeScript(推荐,类型安全)
#
# ? Where to put your end-to-end tests?
# → tests(默认即可)
#
# ? Add a GitHub Actions workflow?
# → true(如果项目用 GitHub)
#
# ? Install Playwright browsers?
# → true(自动下载浏览器)
向导完成后自动生成的目录结构:
bash
your-project/
├── tests/
│ └── example.spec.ts # 示例测试文件
├── tests-examples/
│ └── demo-todo-app.spec.ts # Todo 应用示例
├── playwright.config.ts # 核心配置文件
├── package.json # 依赖已自动添加
├── .github/
│ └── workflows/
│ └── playwright.yml # CI 配置(如选择了)
└── node_modules/
3.3 安装方式二:手动安装(仅作为库使用)
适用场景:写爬虫脚本、自动化工具、不需要测试框架。
bash
# Step 1: 创建项目
mkdir playwright-script
cd playwright-script
npm init -y
# Step 2: 安装 Playwright 库
npm install playwright
# Step 3: 下载浏览器引擎(三选一或全选)
npx playwright install # 下载全部三个浏览器(~500MB)
npx playwright install chromium # 仅 Chromium(~150MB)
npx playwright install firefox # 仅 Firefox(~80MB)
npx playwright install webkit # 仅 WebKit(~50MB)
# Step 4: 验证
node -e "const { chromium } = require('playwright'); console.log('✅ Playwright 已就绪')"
3.4 安装方式三:仅安装测试框架(@playwright/test)
适用场景:已有项目,只需添加 E2E 测试能力。
php
# Step 1: 在已有项目中安装
npm install -D @playwright/test
# Step 2: 下载浏览器
npx playwright install
# Step 3: 手动创建配置文件
cat > playwright.config.ts << 'EOF'
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
},
});
EOF
# Step 4: 创建测试目录
mkdir -p tests
3.5 安装方式四:Python 版 Playwright
适用场景:Python 技术栈团队、数据分析、非前端开发者。
bash
# Step 1: 确认 Python 版本
python3 --version # 需要 >= 3.8
# Step 2: 安装(推荐使用虚拟环境)
python3 -m venv playwright-env
source playwright-env/bin/activate # macOS/Linux
# playwright-env\Scripts\activate # Windows
# Step 3: pip 安装
pip install playwright
# Step 4: 下载浏览器引擎
playwright install
# Step 5: 安装系统依赖(Linux)
playwright install-deps
# Step 6: 验证
python3 -c "from playwright.sync_api import sync_playwright; print('✅ Python Playwright 就绪')"
3.6 安装方式五:Java / .NET 版
python
# ===== Java (Maven) =====
# pom.xml 中添加:
# <dependency>
# <groupId>com.microsoft.playwright</groupId>
# <artifactId>playwright</artifactId>
# <version>1.48.0</version>
# </dependency>
# 下载浏览器
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install"
# ===== .NET (C#) =====
dotnet add package Microsoft.Playwright
pwsh bin/Debug/net8.0/playwright.ps1 install
3.7 国内镜像加速配置
bash
# ===== 方法一:临时环境变量 =====
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright
npx playwright install
# ===== 方法二:项目级 .npmrc =====
echo "PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright" >> .npmrc
npx playwright install
# ===== 方法三:全局配置 =====
npm config set PLAYWRIGHT_DOWNLOAD_HOST https://npmmirror.com/mirrors/playwright
# ===== Python 版镜像 =====
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright
playwright install
3.8 Linux 系统依赖安装
csharp
# ===== 推荐:一键安装(自动检测缺失依赖) =====
npx playwright install-deps
# 仅安装某个浏览器的依赖
npx playwright install-deps chromium
npx playwright install-deps firefox
npx playwright install-deps webkit
# ===== 手动安装(当 install-deps 不可用时) =====
# Ubuntu / Debian
sudo apt-get update && sudo apt-get install -y \
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libdbus-1-3 \
libxkbcommon0 \
libatspi2.0-0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libpango-1.0-0 \
libcairo2 \
libasound2 \
libwayland-client0
# CentOS / RHEL
sudo yum install -y \
nss atk at-spi2-atk cups-libs libdrm libxkbcommon \
libXcomposite libXdamage libXrandr mesa-libgbm \
pango alsa-lib wayland-client
3.9 Docker 环境安装
bash
# ===== 推荐:使用官方预构建镜像 =====
FROM mcr.microsoft.com/playwright:v1.48.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# 运行测试
CMD ["npx", "playwright", "test"]
bash
# 构建并运行
docker build -t pw-tests .
docker run --rm -v $(pwd)/test-results:/app/test-results pw-tests
# ===== 如果必须用自定义基础镜像 =====
FROM node:22-slim
RUN apt-get update && apt-get install -y \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libdbus-1-3 libxkbcommon0 \
libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 \
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci
RUN npx playwright install --with-deps
COPY . .
3.10 安装验证(完整测试脚本)
javascript
# 创建验证脚本
cat > verify-playwright.js << 'EOF'
const { chromium, firefox, webkit } = require('playwright');
(async () => {
const browsers = [
{ name: 'Chromium', engine: chromium },
{ name: 'Firefox', engine: firefox },
{ name: 'WebKit', engine: webkit },
];
for (const { name, engine } of browsers) {
try {
console.log(`🚀 正在测试 ${name}...`);
const browser = await engine.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const title = await page.title();
console.log(` ✅ ${name} 正常 | 页面标题: ${title}`);
await browser.close();
} catch (e) {
console.log(` ⚠️ ${name} 未安装或启动失败: ${e.message}`);
}
}
console.log('\n🎉 Playwright 安装验证完成!');
})();
EOF
node verify-playwright.js
3.11 Playwright 安装注意事项
| 序号 | 注意事项 | 说明 |
|---|---|---|
| 1 | 首次安装耗时长 | 三大浏览器总计约 500MB,耐心等待或使用镜像 |
| 2 | npx playwright install 必须执行 |
仅 npm install 不会下载浏览器 |
| 3 | Linux 必须安装系统依赖 | 否则浏览器启动报 error while loading shared libraries |
| 4 | Docker 推荐官方镜像 | mcr.microsoft.com/playwright 已预装所有依赖 |
| 5 | 不要混用全局和局部安装 | 统一使用项目局部 npx playwright |
| 6 | 版本升级后需重新下载浏览器 | npm update 后执行 npx playwright install |
| 7 | WebKit 在 Linux 上需要额外依赖 | install-deps 会自动处理 |
| 8 | CI 环境加 --with-deps |
npx playwright install --with-deps 一步到位 |
| 9 | 防火墙环境 | 配置 HTTPS_PROXY 或使用离线包 |
| 10 | .gitignore 排除 |
添加 test-results/、playwright-report/、playwright/.cache/ |
3.12 Playwright 卸载与清理
bash
# 卸载包
npm uninstall playwright @playwright/test
# 清理浏览器缓存
rm -rf ~/.cache/ms-playwright
# Windows: %USERPROFILE%\AppData\Local\ms-playwright
# 清理测试产物
rm -rf test-results/ playwright-report/ playwright/.cache/
# 完全清理
rm -rf node_modules package-lock.json
四、Puppeteer 使用命令与实战详解
4.1 使用场景总览
| 场景 | 复杂度 | 典型应用 |
|---|---|---|
| 网页截图 / 缩略图 | ⭐ | 社交媒体预览图、报告配图 |
| PDF 报告生成 | ⭐⭐ | 发票、合同、数据报表 |
| SPA 爬虫 / 数据采集 | ⭐⭐⭐ | 需要 JS 渲染的动态页面 |
| 自动化表单填写 | ⭐⭐⭐ | 内部系统批量操作 |
| 性能监控 / CWV 采集 | ⭐⭐⭐ | LCP、CLS、FCP 指标 |
| 邮件/通知截图 | ⭐ | 动态内容转静态图片 |
| 自动化回归测试 | ⭐⭐⭐ | 搭配 Jest/Mocha |
4.2 启动浏览器(所有操作的起点)
javascript
const puppeteer = require('puppeteer');
// ===== 基础启动 =====
const browser = await puppeteer.launch();
// ===== 有头模式(调试用,能看到浏览器窗口) =====
const browser = await puppeteer.launch({ headless: false });
// ===== 新版无头模式(Chrome 112+,推荐) =====
const browser = await puppeteer.launch({ headless: 'new' });
// ===== 带常用参数启动 =====
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox', // Docker/Linux 必须
'--disable-setuid-sandbox', // Docker/Linux 必须
'--disable-dev-shm-usage', // 防止 /dev/shm 空间不足
'--disable-gpu', // 无 GPU 环境
'--window-size=1920,1080', // 窗口大小
'--lang=zh-CN', // 语言
],
defaultViewport: { width: 1920, height: 1080 },
});
// ===== 指定浏览器路径 =====
const browser = await puppeteer.launch({
executablePath: '/usr/bin/google-chrome-stable',
});
// ===== 连接已运行的浏览器 =====
const browser = await puppeteer.connect({
browserURL: 'http://localhost:9222',
});
// 或
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://localhost:9222/devtools/browser/xxx',
});
// ===== 启动 Firefox(v23+ 实验性) =====
const browser = await puppeteer.launch({ product: 'firefox' });
4.3 页面导航与等待
dart
const page = await browser.newPage();
// ===== 导航 =====
await page.goto('https://example.com');
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.goto('https://example.com', { timeout: 60000 });
// waitUntil 选项说明:
// 'load' → window.onload 触发
// 'domcontentloaded' → DOMContentLoaded 触发
// 'networkidle0' → 500ms 内无网络请求
// 'networkidle2' → 500ms 内最多 2 个网络请求
// ===== 前进/后退/刷新 =====
await page.goBack();
await page.goForward();
await page.reload({ waitUntil: 'networkidle2' });
// ===== 等待 =====
await page.waitForSelector('.result-item');
await page.waitForSelector('#btn', { visible: true, timeout: 10000 });
await page.waitForNavigation({ waitUntil: 'networkidle2' });
await page.waitForResponse(res => res.url().includes('/api/data'));
await page.waitForRequest(req => req.url().includes('/api/submit'));
await page.waitForFunction(() => document.querySelectorAll('.item').length > 10);
await page.waitForTimeout(2000); // 硬等待(尽量避免)
4.4 页面交互操作
csharp
// ===== 点击 =====
await page.click('#submit-btn');
await page.click('a.next-page');
await page.click('.item', { button: 'right' }); // 右键
await page.click('.item', { clickCount: 2 }); // 双击
// ===== 输入 =====
await page.type('#username', 'admin'); // 逐字输入
await page.type('#search', '关键词', { delay: 100 }); // 模拟打字速度
await page.click('#input'); await page.keyboard.type('text'); // 另一种方式
// ===== 键盘操作 =====
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.keyboard.press('Escape');
await page.keyboard.down('Shift');
await page.keyboard.press('ArrowDown');
await page.keyboard.up('Shift');
await page.keyboard.sendCharacter('你好'); // 直接输入中文
// ===== 鼠标操作 =====
await page.mouse.click(100, 200); // 坐标点击
await page.mouse.move(100, 200); // 移动
await page.mouse.down(); // 按下
await page.mouse.up(); // 释放
await page.mouse.wheel({ deltaY: 500 }); // 滚动
// ===== 下拉选择 =====
await page.select('#country', 'CN');
await page.select('#city', 'beijing', 'shanghai'); // 多选
// ===== 复选框/单选框 =====
await page.click('#agree-checkbox');
// ===== 文件上传 =====
const inputElement = await page.$('input[type="file"]');
await inputElement.uploadFile('/path/to/file.pdf');
// ===== 清空输入框 =====
await page.click('#input', { clickCount: 3 }); // 全选
await page.keyboard.press('Backspace'); // 删除
4.5 数据提取
dart
// ===== 获取单个元素文本 =====
const title = await page.$eval('h1', el => el.textContent);
const href = await page.$eval('a.main-link', el => el.href);
// ===== 获取多个元素 =====
const items = await page.$$eval('.product-card', els =>
els.map(el => ({
name: el.querySelector('.name')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
image: el.querySelector('img')?.src,
}))
);
// ===== 在页面上下文中执行任意 JS =====
const data = await page.evaluate(() => {
return {
url: window.location.href,
title: document.title,
allLinks: Array.from(document.querySelectorAll('a')).map(a => a.href),
localStorage: { ...localStorage },
};
});
// ===== 带参数传递 =====
const result = await page.evaluate((selector, count) => {
return document.querySelectorAll(selector).length >= count;
}, '.item', 10);
// ===== 获取元素属性 =====
const element = await page.$('.target');
const className = await page.evaluate(el => el.className, element);
const isVisible = await page.evaluate(el => el.offsetParent !== null, element);
4.6 截图与 PDF
php
// ===== 截图 =====
await page.screenshot({ path: 'full.png', fullPage: true }); // 全页
await page.screenshot({ path: 'viewport.png' }); // 视口
await page.screenshot({ path: 'element.png', clip: { x: 10, y: 10, width: 200, height: 100 } }); // 区域
await page.screenshot({ path: 'quality.jpg', type: 'jpeg', quality: 80 }); // JPEG
// 元素截图
const element = await page.$('.chart');
await element.screenshot({ path: 'chart.png' });
// ===== PDF(仅 Chromium) =====
await page.pdf({
path: 'report.pdf',
format: 'A4', // A4, Letter, Legal 等
landscape: false, // 横向
printBackground: true, // 打印背景色
scale: 0.8, // 缩放
margin: {
top: '20mm',
bottom: '20mm',
left: '15mm',
right: '15mm',
},
headerTemplate: '<div style="font-size:10px; text-align:center; width:100%;">报告标题</div>',
footerTemplate: '<div style="font-size:10px; text-align:center; width:100%;">第 <span class="pageNumber"></span> 页</div>',
displayHeaderFooter: true,
});
4.7 网络拦截与请求控制
javascript
// ===== 开启拦截 =====
await page.setRequestInterception(true);
// ===== 屏蔽资源类型(加速加载) =====
page.on('request', (req) => {
const blocked = ['image', 'font', 'media', 'stylesheet'];
if (blocked.includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
// ===== 修改请求头 =====
page.on('request', (req) => {
req.continue({
headers: {
...req.headers(),
'Authorization': 'Bearer your-token',
'X-Custom-Header': 'value',
},
});
});
// ===== Mock API 响应 =====
page.on('request', (req) => {
if (req.url().includes('/api/users')) {
req.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mock 用户' }]),
});
} else {
req.continue();
}
});
// ===== 监听响应 =====
page.on('response', async (res) => {
if (res.url().includes('/api/')) {
console.log(`[${res.status()}] ${res.url()}`);
// const body = await res.json();
}
});
4.8 Cookie 与存储管理
javascript
// ===== Cookie 操作 =====
await page.setCookie({ name: 'token', value: 'abc123', domain: '.example.com' });
const cookies = await page.cookies();
const specificCookies = await page.cookies('https://example.com');
await page.deleteCookie({ name: 'token', domain: '.example.com' });
// ===== LocalStorage 操作 =====
await page.evaluate(() => {
localStorage.setItem('key', 'value');
});
const value = await page.evaluate(() => localStorage.getItem('key'));
await page.evaluate(() => localStorage.clear());
// ===== 持久化登录态 =====
const fs = require('fs');
// 保存
const cookies = await page.cookies();
fs.writeFileSync('cookies.json', JSON.stringify(cookies));
// 加载
const savedCookies = JSON.parse(fs.readFileSync('cookies.json', 'utf-8'));
await page.setCookie(...savedCookies);
4.9 多页面与 iframe
dart
// ===== 多标签页 =====
const page1 = await browser.newPage();
const page2 = await browser.newPage();
await page1.goto('https://site-a.com');
await page2.goto('https://site-b.com');
// 监听新打开的标签页
browser.on('targetcreated', async (target) => {
if (target.type() === 'page') {
const newPage = await target.page();
console.log('新标签页:', await newPage.title());
}
});
// ===== iframe 操作 =====
// 获取 frame
const frame = page.frames().find(f => f.name() === 'my-iframe');
const frame = page.frames().find(f => f.url().includes('payment'));
// 在 frame 中操作
await frame.click('#submit');
await frame.type('#input', 'text');
// 嵌套 iframe
const childFrame = frame.childFrames()[0];
await childFrame.click('#inner-btn');
4.10 性能与调试
javascript
// ===== 开启 DevTools(有头模式) =====
const browser = await puppeteer.launch({
headless: false,
devtools: true, // 自动打开 DevTools
});
// ===== CDP 直接访问 =====
const client = await page.target().createCDPSession();
await client.send('Performance.enable');
const { metrics } = await client.send('Performance.getMetrics');
console.table(metrics);
// ===== 网络性能 =====
await page.setCacheEnabled(false); // 禁用缓存测试
const startTime = Date.now();
await page.goto(url, { waitUntil: 'load' });
console.log(`加载耗时: ${Date.now() - startTime}ms`);
// ===== 控制台日志监听 =====
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
page.on('pageerror', err => console.error('PAGE ERROR:', err.message));
page.on('requestfailed', req => console.log('FAILED:', req.url(), req.failure()?.errorText));
// ===== 开启 tracing =====
await page.tracing.start({ path: 'trace.json', screenshots: true });
// ... 执行操作 ...
await page.tracing.stop();
4.11 Puppeteer 常用命令速查表
| 操作 | 命令/代码 |
|---|---|
| 启动浏览器 | puppeteer.launch() |
| 新建页面 | browser.newPage() |
| 导航 | page.goto(url, options) |
| 截图 | page.screenshot({ path, fullPage }) |
| 生成 PDF | page.pdf({ path, format }) |
| 点击 | page.click(selector) |
| 输入 | page.type(selector, text) |
| 按键 | page.keyboard.press(key) |
| 等待元素 | page.waitForSelector(sel, opts) |
| 等待导航 | page.waitForNavigation() |
| 等待响应 | page.waitForResponse(pred) |
| 执行 JS | page.evaluate(fn, ...args) |
| 获取文本 | page.$eval(sel, el => el.textContent) |
| 获取多元素 | page.$$eval(sel, els => els.map(...)) |
| 设置 Cookie | page.setCookie(cookie) |
| 获取 Cookie | page.cookies() |
| 设置视口 | page.setViewport({ width, height }) |
| 设置 UA | page.setUserAgent(ua) |
| 开启拦截 | page.setRequestInterception(true) |
| 关闭浏览器 | browser.close() |
| 关闭页面 | page.close() |
| 获取所有页面 | browser.pages() |
| 获取浏览器版本 | browser.version() |
五、Playwright 使用命令与实战详解
5.1 使用场景总览
| 场景 | 复杂度 | 典型应用 |
|---|---|---|
| 跨浏览器 E2E 测试 | ⭐⭐⭐ | 一套代码验证 Chrome/Firefox/Safari |
| 视觉回归测试 | ⭐⭐⭐ | 截图对比,检测 UI 变更 |
| API + UI 混合测试 | ⭐⭐⭐ | 先调 API 准备数据,再验证 UI |
| 移动端适配验证 | ⭐⭐ | 内置 100+ 设备描述符 |
| 多租户/多角色并行测试 | ⭐⭐⭐⭐ | BrowserContext 天然隔离 |
| 爬虫 / 数据采集 | ⭐⭐ | 自动等待让爬虫更稳定 |
| 无障碍(A11y)测试 | ⭐⭐ | 结合 axe-core |
| CI/CD 流水线集成 | ⭐⭐⭐ | GitHub Actions / GitLab CI |
5.2 CLI 命令大全(npx playwright)
ini
# ===== 浏览器管理 =====
npx playwright install # 安装所有浏览器
npx playwright install chromium # 仅安装 Chromium
npx playwright install firefox # 仅安装 Firefox
npx playwright install webkit # 仅安装 WebKit
npx playwright install --with-deps # 安装浏览器 + 系统依赖
npx playwright install-deps # 仅安装系统依赖
npx playwright install --dry-run # 预览将安装什么(不实际执行)
npx playwright install --list # 列出已安装的浏览器
# ===== 运行测试 =====
npx playwright test # 运行所有测试
npx playwright test tests/login.spec.ts # 运行指定文件
npx playwright test -g "登录成功" # 按测试名称过滤
npx playwright test --project=chromium # 仅 Chromium
npx playwright test --project=firefox # 仅 Firefox
npx playwright test --project=webkit # 仅 WebKit
npx playwright test --headed # 有头模式(看到浏览器)
npx playwright test --debug # 调试模式(Inspector)
npx playwright test --ui # UI 模式(图形界面)
npx playwright test --workers=1 # 单线程(调试用)
npx playwright test --workers=8 # 8 个并行
npx playwright test --repeat-each=5 # 每个测试重复 5 次
npx playwright test --retries=3 # 失败重试 3 次
npx playwright test --timeout=60000 # 超时 60 秒
npx playwright test --grep "smoke" # 按 tag 过滤
npx playwright test --grep-invert "slow" # 排除某些测试
npx playwright test --list # 仅列出测试(不运行)
npx playwright test --last-failed # 仅重跑上次失败的
npx playwright test --shard=1/3 # 分片(CI 并行)
npx playwright test --reporter=html # HTML 报告
npx playwright test --reporter=json # JSON 报告
npx playwright test --reporter=junit # JUnit 报告
npx playwright test --update-snapshots # 更新截图基准
npx playwright test --trace on # 开启 trace
npx playwright test --video on # 录制视频
npx playwright test --screenshot on # 每步截图
# ===== 代码生成(Codegen) =====
npx playwright codegen https://example.com
npx playwright codegen --target=python https://example.com
npx playwright codegen --target=javascript https://example.com
npx playwright codegen --target=csharp https://example.com
npx playwright codegen --target=java https://example.com
npx playwright codegen --device="iPhone 14 Pro" https://example.com
npx playwright codegen --viewport-size=1920,1080 https://example.com
npx playwright codegen --save-storage=auth.json https://example.com
npx playwright codegen --load-storage=auth.json https://example.com
npx playwright codegen --lang=zh-CN https://example.com
npx playwright codegen -o tests/generated.spec.ts https://example.com
npx playwright codegen --browser=firefox https://example.com
npx playwright codegen --color-scheme=dark https://example.com
# ===== 报告与调试 =====
npx playwright show-report # 打开 HTML 报告
npx playwright show-report ./report-dir # 指定报告目录
npx playwright show-trace trace.zip # 打开 Trace Viewer
npx playwright show-trace --port=8080 # 指定端口
# ===== 截图 =====
npx playwright screenshot https://example.com out.png
npx playwright screenshot --full-page https://example.com full.png
npx playwright screenshot --viewport-size=375,812 https://example.com mobile.png
npx playwright screenshot --browser=firefox https://example.com ff.png
npx playwright screenshot --wait-for-timeout=3000 https://example.com delayed.png
# ===== PDF =====
npx playwright pdf https://example.com page.pdf # 仅 Chromium
# ===== 其他 =====
npx playwright --version # 查看版本
npx playwright --help # 帮助
npx playwright test --help # 测试命令帮助
npx playwright codegen --help # Codegen 帮助
5.3 启动浏览器
php
const { chromium, firefox, webkit } = require('playwright');
// ===== 基础启动 =====
const browser = await chromium.launch();
const browser = await firefox.launch();
const browser = await webkit.launch();
// ===== 有头模式 =====
const browser = await chromium.launch({ headless: false });
// ===== 慢动作(每步操作间隔 500ms,调试用) =====
const browser = await chromium.launch({ slowMo: 500 });
// ===== 带参数启动 =====
const browser = await chromium.launch({
headless: false,
args: ['--start-maximized', '--lang=zh-CN'],
});
// ===== 使用系统 Chrome / Edge =====
const browser = await chromium.launch({ channel: 'chrome' });
const browser = await chromium.launch({ channel: 'msedge' });
// ===== 连接已运行的浏览器 =====
const browser = await chromium.connectOverCDP('http://localhost:9222');
const browser = await chromium.connect('ws://localhost:3000');
// ===== 创建浏览器上下文(核心概念) =====
const context = await browser.newContext(); // 默认上下文
const context = await browser.newContext({ // 自定义上下文
viewport: { width: 1280, height: 720 },
userAgent: 'Custom UA',
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
geolocation: { latitude: 39.9, longitude: 116.4 },
permissions: ['geolocation'],
colorScheme: 'dark',
extraHTTPHeaders: { 'X-Custom': 'value' },
});
// ===== 使用设备描述符 =====
const { devices } = require('playwright');
const iPhone = devices['iPhone 14 Pro'];
const pixel = devices['Pixel 7'];
const iPad = devices['iPad Pro 11'];
const context = await browser.newContext({ ...iPhone });
// ===== 列出所有可用设备 =====
console.log(Object.keys(devices));
// ===== 创建页面 =====
const page = await context.newPage();
5.4 页面导航与等待
javascript
// ===== 导航 =====
await page.goto('https://example.com');
await page.goto('https://example.com', { waitUntil: 'load' });
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
await page.goto('https://example.com', { timeout: 60000 });
// ===== 前进/后退/刷新 =====
await page.goBack();
await page.goForward();
await page.reload();
await page.reload({ waitUntil: 'networkidle' });
// ===== 等待(Playwright 大部分操作自动等待,以下是显式等待) =====
await page.waitForURL('**/dashboard');
await page.waitForURL(url => url.includes('success'));
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000); // 硬等待(尽量避免)
// 等待选择器
await page.waitForSelector('.loaded');
await page.waitForSelector('.modal', { state: 'visible' });
await page.waitForSelector('.spinner', { state: 'hidden' });
await page.waitForSelector('.item', { state: 'attached' });
await page.waitForSelector('.item', { state: 'detached' });
// 等待响应/请求
await page.waitForResponse('**/api/data');
await page.waitForResponse(res => res.status() === 200);
await page.waitForRequest('**/api/submit');
// 等待函数
await page.waitForFunction(() => document.readyState === 'complete');
await page.waitForFunction(() => window.itemsLoaded === true);
5.5 定位器系统(Playwright 核心优势)
php
// ===== 推荐定位方式(按优先级排序) =====
// 1. Role(最推荐,语义化 + 无障碍)
page.getByRole('button', { name: '提交' });
page.getByRole('link', { name: '首页' });
page.getByRole('heading', { name: '欢迎' });
page.getByRole('textbox', { name: '用户名' });
page.getByRole('checkbox', { name: '记住我' });
page.getByRole('tab', { name: '设置' });
page.getByRole('dialog');
page.getByRole('alert');
// 2. 文本
page.getByText('欢迎回来');
page.getByText('欢迎回来', { exact: true }); // 精确匹配
// 3. Label(表单)
page.getByLabel('用户名');
page.getByLabel('密码');
page.getByLabel('电子邮箱');
// 4. Placeholder
page.getByPlaceholder('请输入搜索关键词');
// 5. Alt text(图片)
page.getByAltText('公司 Logo');
// 6. Title
page.getByTitle('关闭对话框');
// 7. Test ID(需前端配合添加 data-testid)
page.getByTestId('submit-button');
page.getByTestId('user-profile-card');
// ===== CSS / XPath 定位(兜底) =====
page.locator('.class-name');
page.locator('#id');
page.locator('div > span.text');
page.locator('css=.complex >> nth=0');
page.locator('xpath=//div[@class="item"]');
// ===== 定位器链式过滤 =====
page.locator('.product-card').filter({ hasText: 'iPhone' });
page.locator('.product-card').filter({ has: page.locator('.badge-new') });
page.locator('tr').filter({ hasText: '张三' }).locator('td').nth(2);
// ===== 定位器操作 =====
const btn = page.getByRole('button', { name: '提交' });
await btn.click();
await btn.dblclick();
await btn.rightClick();
await btn.hover();
await btn.focus();
await btn.press('Enter');
await btn.check(); // 勾选
await btn.uncheck(); // 取消勾选
await btn.setEnabled(); // 等待可用
// ===== 输入 =====
await page.getByLabel('用户名').fill('admin');
await page.getByLabel('密码').fill('pass123');
await page.getByLabel('备注').pressSequentially('逐字输入', { delay: 50 });
await page.getByLabel('搜索').clear();
await page.getByLabel('搜索').type('old way'); // 不推荐
// ===== 选择 =====
await page.getByLabel('城市').selectOption('beijing');
await page.getByLabel('城市').selectOption({ label: '北京' });
await page.getByLabel('城市').selectOption({ value: 'bj' });
await page.getByLabel('城市').selectOption({ index: 0 });
// ===== 文件上传 =====
await page.getByLabel('上传文件').setInputFiles('./file.pdf');
await page.getByLabel('上传文件').setInputFiles(['./a.png', './b.png']);
await page.getByLabel('上传文件').setInputFiles([]); // 清空
// ===== 获取信息 =====
const text = await page.getByRole('heading').textContent();
const innerText = await page.locator('.content').innerText();
const innerHTML = await page.locator('.content').innerHTML();
const value = await page.getByLabel('用户名').inputValue();
const isVisible = await page.locator('.modal').isVisible();
const isEnabled = await page.getByRole('button').isEnabled();
const isChecked = await page.getByRole('checkbox').isChecked();
const count = await page.locator('.item').count();
const allTexts = await page.locator('.item').allTextContents();
const attr = await page.locator('a').getAttribute('href');
5.6 网络拦截与 Mock
javascript
// ===== 拦截并 Mock 响应 =====
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: '张三', role: 'admin' },
{ id: 2, name: '李四', role: 'user' },
]),
});
});
// ===== 拦截并修改请求 =====
await page.route('**/api/**', route => {
const headers = {
...route.request().headers(),
'Authorization': 'Bearer mock-token',
};
route.continue({ headers });
});
// ===== 屏蔽资源 =====
await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', route => route.abort());
await page.route('**/*', route => {
if (['image', 'font', 'media'].includes(route.request().resourceType())) {
route.abort();
} else {
route.continue();
}
});
// ===== 模拟网络延迟 =====
await page.route('**/api/slow', async route => {
await new Promise(r => setTimeout(r, 3000));
route.continue();
});
// ===== 模拟错误 =====
await page.route('**/api/data', route => {
route.fulfill({ status: 500, body: 'Internal Server Error' });
});
// ===== 从文件读取 Mock 数据 =====
const fs = require('fs');
await page.route('**/api/config', route => {
const data = fs.readFileSync('./mocks/config.json', 'utf-8');
route.fulfill({ contentType: 'application/json', body: data });
});
// ===== 监听网络事件 =====
page.on('request', req => console.log('→', req.method(), req.url()));
page.on('response', res => console.log('←', res.status(), res.url()));
page.on('requestfailed', req => console.log('✗', req.url(), req.failure()?.errorText));
// ===== 获取响应体 =====
const response = await page.waitForResponse('**/api/data');
const json = await response.json();
const text = await response.text();
5.7 多标签页 / 弹窗 / iframe
javascript
// ===== 新标签页 =====
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.click('a[target="_blank"]'),
]);
await newPage.waitForLoadState();
console.log(await newPage.title());
// ===== 弹窗(Dialog) =====
page.on('dialog', async dialog => {
console.log(dialog.type()); // alert / confirm / prompt
console.log(dialog.message());
await dialog.accept(); // 确认
// await dialog.dismiss(); // 取消
// await dialog.accept('输入内容'); // prompt 填写
});
// ===== Popup 窗口 =====
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.click('#open-popup'),
]);
await popup.waitForLoadState();
// ===== iframe =====
// 方式一:frameLocator(推荐)
const iframe = page.frameLocator('#payment-frame');
await iframe.locator('#card-number').fill('4111111111111111');
await iframe.locator('#submit').click();
// 方式二:嵌套 iframe
const outer = page.frameLocator('#outer-frame');
const inner = outer.frameLocator('#inner-frame');
await inner.locator('#btn').click();
// 方式三:frame 对象
const frame = page.frame({ name: 'myFrame' });
const frame = page.frame({ url: /payment/ });
5.8 文件下载处理
javascript
// ===== 下载文件 =====
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#download-btn'),
]);
console.log('文件名:', download.suggestedFilename());
console.log('URL:', download.url());
// 保存到指定路径
await download.saveAs('./downloads/' + download.suggestedFilename());
// 获取下载流
const stream = await download.createReadStream();
// 取消下载
await download.cancel();
// 删除临时文件
await download.delete();
5.9 认证状态管理(storageState)
php
// ===== 保存登录态 =====
// 登录后执行:
await context.storageState({ path: '.auth/user.json' });
// ===== 复用登录态 =====
const context = await browser.newContext({
storageState: '.auth/user.json',
});
// ===== 在 playwright.config.ts 中配置 =====
// projects: [
// { name: 'setup', testMatch: /auth.setup.ts/ },
// {
// name: 'authenticated',
// use: { storageState: '.auth/user.json' },
// dependencies: ['setup'],
// },
// ]
// ===== 未登录状态 =====
const context = await browser.newContext({
storageState: { cookies: [], origins: [] },
});
5.10 移动端与设备模拟
php
const { chromium, devices } = require('playwright');
// ===== 使用内置设备描述符 =====
const iPhone = devices['iPhone 14 Pro'];
const pixel = devices['Pixel 7'];
const iPad = devices['iPad Pro 11'];
const galaxy = devices['Galaxy S9+'];
const context = await browser.newContext({
...iPhone,
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
geolocation: { latitude: 31.23, longitude: 121.47 }, // 上海
permissions: ['geolocation'],
});
// ===== 自定义模拟 =====
const context = await browser.newContext({
viewport: { width: 375, height: 812 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...)',
});
// ===== 模拟地理位置 =====
await context.setGeolocation({ latitude: 39.9, longitude: 116.4 });
await context.grantPermissions(['geolocation']);
// ===== 模拟暗色模式 =====
const context = await browser.newContext({ colorScheme: 'dark' });
// ===== 模拟打印媒体 =====
const context = await browser.newContext({ media: 'print' });
// ===== 模拟离线 =====
await context.setOffline(true);
// ... 测试离线行为 ...
await context.setOffline(false);
5.11 截图与视觉对比
php
// ===== 截图 =====
await page.screenshot({ path: 'full.png', fullPage: true });
await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'area.png', clip: { x: 0, y: 0, width: 500, height: 300 } });
// 元素截图
await page.locator('.chart').screenshot({ path: 'chart.png' });
// ===== 视觉回归测试(内置) =====
// 首次运行生成基准图,后续对比
await expect(page).toHaveScreenshot('homepage.png');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100, // 允许 100 像素差异
maxDiffPixelRatio: 0.01, // 允许 1% 差异
threshold: 0.2, // 颜色阈值
animations: 'disabled', // 禁用动画
mask: [page.locator('.dynamic-time')], // 遮罩动态区域
});
// 元素级对比
await expect(page.locator('.card')).toHaveScreenshot('card.png');
// 更新基准:npx playwright test --update-snapshots
5.12 Playwright 常用命令速查表
| 操作 | 命令/代码 |
|---|---|
| 启动浏览器 | chromium.launch() |
| 新建上下文 | browser.newContext(opts) |
| 新建页面 | context.newPage() |
| 导航 | page.goto(url) |
| 点击 | locator.click() |
| 填写 | locator.fill(text) |
| 按键 | locator.press('Enter') |
| 选择 | locator.selectOption(val) |
| 等待可见 | locator.waitFor({ state: 'visible' }) |
| 等待 URL | page.waitForURL(pattern) |
| 获取文本 | locator.textContent() |
| 获取数量 | locator.count() |
| 截图 | page.screenshot(opts) |
| 视觉断言 | expect(page).toHaveScreenshot() |
| Mock 路由 | page.route(pattern, handler) |
| 下载 | page.waitForEvent('download') |
| 弹窗 | page.on('dialog', handler) |
| iframe | page.frameLocator(sel) |
| 设备模拟 | devices['iPhone 14 Pro'] |
| 保存状态 | context.storageState({ path }) |
| 关闭 | browser.close() |
六、Playwright Test:企业级 E2E 测试
6.1 编写测试
dart
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('登录模块', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('正常登录成功', async ({ page }) => {
await page.getByLabel('用户名').fill('admin');
await page.getByLabel('密码').fill('password123');
await page.getByRole('button', { name: '登录' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('欢迎, admin')).toBeVisible();
});
test('密码错误显示提示', async ({ page }) => {
await page.getByLabel('用户名').fill('admin');
await page.getByLabel('密码').fill('wrong');
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText('密码错误')).toBeVisible();
});
test('空表单提交显示校验', async ({ page }) => {
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText('请输入用户名')).toBeVisible();
await expect(page.getByText('请输入密码')).toBeVisible();
});
test('登录后可以访问受保护页面', async ({ page }) => {
await page.getByLabel('用户名').fill('admin');
await page.getByLabel('密码').fill('password123');
await page.getByRole('button', { name: '登录' }).click();
await page.goto('/settings');
await expect(page.getByRole('heading', { name: '设置' })).toBeVisible();
});
});
6.2 常用断言
scss
// 可见性
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
// 文本
await expect(locator).toHaveText('精确文本');
await expect(locator).toContainText('包含');
await expect(locator).toHaveText(/正则/);
// 输入值
await expect(input).toHaveValue('admin');
// 状态
await expect(checkbox).toBeChecked();
await expect(checkbox).not.toBeChecked();
await expect(button).toBeEnabled();
await expect(button).toBeDisabled();
// URL / 标题
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle('首页');
// 截图对比
await expect(page).toHaveScreenshot();
await expect(locator).toHaveScreenshot('element.png');
// 元素数量
await expect(page.locator('.item')).toHaveCount(5);
// CSS 类
await expect(locator).toHaveClass(/active/);
// API 响应
const response = await page.request.get('/api/users');
await expect(response).toBeOK();
await expect(response).toHaveStatus(200);
6.3 配置文件详解
php
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
video: 'on-first-retry',
trace: 'on-first-retry',
actionTimeout: 10_000,
navigationTimeout: 30_000,
},
projects: [
{ name: 'setup', testMatch: /auth.setup.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 14 Pro'] },
},
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
6.4 运行测试命令汇总
bash
npx playwright test # 全部
npx playwright test --project=chromium # 单浏览器
npx playwright test tests/login.spec.ts # 单文件
npx playwright test -g "登录" # 按名称
npx playwright test --headed # 有头
npx playwright test --debug # 调试
npx playwright test --ui # UI 模式
npx playwright test --workers=1 # 串行
npx playwright test --repeat-each=10 # 压力测试
npx playwright test --last-failed # 重跑失败
npx playwright test --update-snapshots # 更新截图
npx playwright test --trace on --video on # 全量记录
npx playwright show-report # 查看报告
七、调试利器(Playwright 专属)
7.1 Codegen:录制生成代码
arduino
npx playwright codegen https://your-app.com
高级用法:
ini
npx playwright codegen --target=python https://your-app.com
npx playwright codegen --device="iPhone 14 Pro" https://your-app.com
npx playwright codegen --save-storage=auth.json https://your-app.com
npx playwright codegen --load-storage=auth.json https://your-app.com
npx playwright codegen -o tests/generated.spec.ts https://your-app.com
npx playwright codegen --browser=firefox https://your-app.com
npx playwright codegen --color-scheme=dark https://your-app.com
7.2 Trace Viewer:时间旅行调试
sql
npx playwright test --trace on
npx playwright show-trace trace.zip
7.3 Inspector:逐步调试
bash
npx playwright test --debug
7.4 UI Mode:可视化测试管理
bash
npx playwright test --ui
八、提高开发效率的 10 个实战技巧
(内容与原文一致,此处省略重复,保持原有 10 个技巧不变)
九、常见场景速查表
| 需求 | Puppeteer | Playwright |
|---|---|---|
| 网页截图 | page.screenshot() |
page.screenshot() |
| 生成 PDF | page.pdf() |
page.pdf()(仅 Chromium) |
| 表单填写 | page.type() / page.click() |
locator.fill() / locator.click() |
| 等待元素 | page.waitForSelector() |
自动 / locator.waitFor() |
| 执行页面 JS | page.evaluate() |
page.evaluate() |
| 拦截请求 | page.setRequestInterception(true) |
page.route() |
| 下载文件 | 手动处理 | download 事件 + download.saveAs() |
| 处理弹窗 | page.on('dialog') |
page.on('dialog') |
| iframe 操作 | page.frames() |
page.frameLocator() |
| 文件上传 | elementHandle.uploadFile() |
locator.setInputFiles() |
| 认证状态持久化 | 手动 page.cookies() |
context.storageState() |
| 设备模拟 | 手动设置 UA + viewport | devices['xxx'] 一行搞定 |
| 视觉回归 | 需第三方库 | 内置 toHaveScreenshot() |
十、常见问题排查
Q1:Chromium / 浏览器下载失败
bash
# Puppeteer
export PUPPETEER_DOWNLOAD_BASE_URL=https://npmmirror.com/mirrors/chrome-for-testing
npm install puppeteer
# Playwright
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright
npx playwright install
Q2:Linux 报错缺少共享库
bash
# Playwright 一键修复
npx playwright install-deps
# Puppeteer 手动安装(见第二章 2.6 节)
Q3:Docker 中运行
bash
# Playwright 推荐官方镜像
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/playwright:v1.48.0-noble npx playwright test
# Puppeteer 需要 --no-sandbox
Q4:元素定位不到
arduino
// 1. 是否在 iframe 中
// 2. 是否被遮挡
// 3. 是否需要等待
// 4. 使用 --debug 模式排查
Q5:测试不稳定(Flaky Test)
markdown
1. 开启 trace → 查看失败截图
2. 检查竞态条件
3. --repeat-each=10 复现
4. 增加重试 + 日志
十一、总结与选型建议
你的场景是什么?
│
├─ 快速截图 / 生成 PDF / 简单爬虫
│ └─→ Puppeteer(轻量、够用、上手快)
│
├─ 跨浏览器 E2E 测试
│ └─→ Playwright(唯一正解)
│
├─ 企业级自动化测试平台
│ └─→ Playwright Test(内置框架 + Trace + 并行 + 报告)
│
├─ 需要支持 Python / Java / .NET
│ └─→ Playwright(多语言 SDK)
│
└─ 已有大量 Puppeteer 代码
└─→ 继续使用 Puppeteer,迁移不紧急
📝 本文所有命令均基于 Puppeteer v23+ 和 Playwright v1.48+ 验证,适用于 Node.js 20/22 LTS。
官方文档:
- Puppeteer:pptr.dev
- Playwright:playwright.dev
祝你自动化之路顺利,效率翻倍!🚀