Files
2026-07-13 21:36:47 +08:00

554 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运行自定义 Playwright 代码
> **何时使用**:当 CLI 命令不够用时——地理定位、权限、媒体模拟、等待策略、iframe 交互、文件下载、剪贴板访问、复杂的多步骤工作流,或任何需要完整 Playwright API 的场景。
> **前置条件**[core-commands.md](core-commands.md) 了解基本的 CLI 用法
## 快速参考
```bash
# 语法:run-code 接受一个以 page 为参数的 async 函数
playwright-cli run-code "async page => {
// 此处可使用完整的 Playwright API
// page.context() 用于浏览器上下文操作
// 返回一个值即可在输出中查看
return await page.title();
}"
```
## 地理定位
覆盖浏览器报告的位置——对于测试基于位置的功能(如门店查找器、配送区域和天气应用)至关重要。
```bash
# 授予权限并将位置设置为纽约
playwright-cli run-code "async page => {
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 40.7128, longitude: -74.0060 });
}"
# 伦敦
playwright-cli run-code "async page => {
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
}"
# 旧金山
playwright-cli run-code "async page => {
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
}"
# 东京
playwright-cli run-code "async page => {
await page.context().grantPermissions(['geolocation']);
await page.context().setGeolocation({ latitude: 35.6762, longitude: 139.6503 });
}"
# 在会话中更新位置(模拟用户移动)
playwright-cli run-code "async page => {
await page.context().setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
}"
# 清除地理定位覆盖
playwright-cli run-code "async page => {
await page.context().clearPermissions();
}"
```
## 权限
控制浏览器权限授予——通知、摄像头、麦克风、剪贴板等。
```bash
# 授予多个权限
playwright-cli run-code "async page => {
await page.context().grantPermissions([
'geolocation',
'notifications',
'camera',
'microphone'
]);
}"
# 仅针对特定来源授予权限
playwright-cli run-code "async page => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], {
origin: 'https://example.com'
});
}"
# 撤销所有权限
playwright-cli run-code "async page => {
await page.context().clearPermissions();
}"
```
可用权限:`geolocation``notifications``camera``microphone``clipboard-read``clipboard-write``payment-handler``midi``midi-sysex``ambient-light-sensor``accelerometer``gyroscope``magnetometer``accessibility-events``background-sync`
## 媒体模拟
测试你的应用在不同媒体条件下的表现——深色模式、减弱动效、打印布局。
### 色彩方案
```bash
# 深色模式
playwright-cli run-code "async page => {
await page.emulateMedia({ colorScheme: 'dark' });
}"
# 浅色模式
playwright-cli run-code "async page => {
await page.emulateMedia({ colorScheme: 'light' });
}"
# 系统偏好(无覆盖)
playwright-cli run-code "async page => {
await page.emulateMedia({ colorScheme: 'no-preference' });
}"
```
### 减弱动效
```bash
# 模拟 prefers-reduced-motion: reduce
playwright-cli run-code "async page => {
await page.emulateMedia({ reducedMotion: 'reduce' });
}"
# 重置为无偏好
playwright-cli run-code "async page => {
await page.emulateMedia({ reducedMotion: 'no-preference' });
}"
```
### 强制色彩(高对比度)
```bash
playwright-cli run-code "async page => {
await page.emulateMedia({ forcedColors: 'active' });
}"
```
### 打印媒体
```bash
# 模拟打印样式表(在生成 PDF 前使用)
playwright-cli run-code "async page => {
await page.emulateMedia({ media: 'print' });
}"
# 重置为屏幕模式
playwright-cli run-code "async page => {
await page.emulateMedia({ media: 'screen' });
}"
```
### 组合多个模拟
```bash
playwright-cli run-code "async page => {
await page.emulateMedia({
colorScheme: 'dark',
reducedMotion: 'reduce',
forcedColors: 'none'
});
}"
```
## 区域设置和时区
覆盖浏览器区域设置和时区以测试国际化:
```bash
# 设置区域设置(影响数字格式、日期显示等)
playwright-cli run-code "async page => {
// 区域设置在上下文创建时设定;对于现有上下文,使用 evaluate
return await page.evaluate(() => navigator.language);
}"
# 对于新上下文,在打开时设置区域设置和时区:
# playwright-cli open --config=locale-config.json
# 其中 locale-config.json 包含:{ "locale": "de-DE", "timezoneId": "Europe/Berlin" }
```
## 等待策略
当页面在操作后需要时间加载、渲染或稳定时使用。
### 等待加载状态
```bash
# 等待所有网络请求完成
playwright-cli run-code "async page => {
await page.waitForLoadState('networkidle');
}"
# 等待 DOM 内容加载完成
playwright-cli run-code "async page => {
await page.waitForLoadState('domcontentloaded');
}"
# 等待完全加载(包括图片、样式表)
playwright-cli run-code "async page => {
await page.waitForLoadState('load');
}"
```
### 等待元素
```bash
# 等待加载旋转器消失
playwright-cli run-code "async page => {
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
}"
# 等待内容出现
playwright-cli run-code "async page => {
await page.waitForSelector('.search-results', { state: 'visible', timeout: 10000 });
}"
# 等待元素从 DOM 中完全移除
playwright-cli run-code "async page => {
await page.waitForSelector('.skeleton-loader', { state: 'detached' });
}"
```
### 等待 URL 变化
```bash
playwright-cli run-code "async page => {
await page.waitForURL('**/dashboard');
}"
playwright-cli run-code "async page => {
await page.waitForURL(/.*\/order\/\d+/);
}"
```
### 等待自定义条件
```bash
# 等待 JavaScript 变量被设置
playwright-cli run-code "async page => {
await page.waitForFunction(() => window.appReady === true);
}"
# 等待特定数量的元素
playwright-cli run-code "async page => {
await page.waitForFunction(() => document.querySelectorAll('.item').length >= 10);
}"
# 轮询等待
playwright-cli run-code "async page => {
await page.waitForFunction(
() => document.querySelector('.status')?.textContent === 'Complete',
{ polling: 500, timeout: 30000 }
);
}"
```
### 等待网络请求
```bash
# 等待特定 API 调用完成
playwright-cli run-code "async page => {
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button#load-users');
const response = await responsePromise;
return { status: response.status(), url: response.url() };
}"
# 等待发起请求
playwright-cli run-code "async page => {
const requestPromise = page.waitForRequest('**/api/submit');
await page.click('button#submit');
const request = await requestPromise;
return request.postDataJSON();
}"
```
## 框架和 Iframe
与 iframe 内部的内容交互:
```bash
# 点击 iframe 内的按钮
playwright-cli run-code "async page => {
const frame = page.locator('iframe#my-iframe').contentFrame();
await frame.locator('button.submit').click();
}"
# 填写 iframe 内的表单
playwright-cli run-code "async page => {
const frame = page.locator('iframe[name=\"checkout\"]').contentFrame();
await frame.locator('input[name=\"card-number\"]').fill('4111111111111111');
await frame.locator('input[name=\"expiry\"]').fill('12/25');
await frame.locator('input[name=\"cvc\"]').fill('123');
}"
# 获取所有框架的 URL
playwright-cli run-code "async page => {
const frames = page.frames();
return frames.map(f => ({ name: f.name(), url: f.url() }));
}"
# 嵌套 iframe
playwright-cli run-code "async page => {
const outerFrame = page.locator('iframe#outer').contentFrame();
const innerFrame = outerFrame.locator('iframe#inner').contentFrame();
await innerFrame.locator('button').click();
}"
```
## 文件下载
```bash
# 触发下载并保存文件
playwright-cli run-code "async page => {
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('a.download-link')
]);
const filename = download.suggestedFilename();
await download.saveAs('./downloads/' + filename);
return '已下载:' + filename;
}"
# 自定义路径下载
playwright-cli run-code "async page => {
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export CSV' }).click()
]);
await download.saveAs('/tmp/export.csv');
return '已保存至 /tmp/export.csv';
}"
# 获取下载 URL(不保存)
playwright-cli run-code "async page => {
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('#download-btn')
]);
return download.url();
}"
```
## 剪贴板
```bash
# 读取剪贴板内容
playwright-cli run-code "async page => {
await page.context().grantPermissions(['clipboard-read']);
return await page.evaluate(() => navigator.clipboard.readText());
}"
# 写入剪贴板
playwright-cli run-code "async page => {
await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!');
}"
# 将元素中的文本复制到剪贴板
playwright-cli run-code "async page => {
const text = await page.locator('.api-key').textContent();
await page.evaluate(t => navigator.clipboard.writeText(t), text);
return '已复制:' + text;
}"
```
## 页面信息
```bash
# 页面标题
playwright-cli run-code "async page => {
return await page.title();
}"
# 当前 URL
playwright-cli run-code "async page => {
return page.url();
}"
# 完整页面 HTML 内容
playwright-cli run-code "async page => {
return await page.content();
}"
# 视口大小
playwright-cli run-code "async page => {
return page.viewportSize();
}"
# 浏览器信息
playwright-cli run-code "async page => {
return await page.evaluate(() => ({
userAgent: navigator.userAgent,
language: navigator.language,
languages: navigator.languages,
cookiesEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
platform: navigator.platform,
screenSize: { width: screen.width, height: screen.height }
}));
}"
```
## JavaScript 执行
```bash
# 执行并返回结果
playwright-cli run-code "async page => {
return await page.evaluate(() => {
return {
title: document.title,
url: window.location.href,
elementCount: document.querySelectorAll('*').length,
scripts: document.querySelectorAll('script').length
};
});
}"
# 向 evaluate 传递参数
playwright-cli run-code "async page => {
const selector = '.product-card';
const count = await page.evaluate(
sel => document.querySelectorAll(sel).length,
selector
);
return '找到了 ' + count + ' 个产品';
}"
# 修改 DOM
playwright-cli run-code "async page => {
await page.evaluate(() => {
document.querySelector('.banner')?.remove();
document.body.style.zoom = '80%';
});
}"
```
## 错误处理
```bash
# 对可选元素使用 try-catch
playwright-cli run-code "async page => {
try {
await page.click('.cookie-consent-accept', { timeout: 2000 });
return 'Cookie 横幅已关闭';
} catch (e) {
return '未找到 Cookie 横幅';
}
}"
# 重试模式
playwright-cli run-code "async page => {
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await page.click('.flaky-button', { timeout: 3000 });
return '在第 ' + attempt + ' 次尝试时点击成功';
} catch (e) {
if (attempt === 3) throw e;
await page.waitForTimeout(1000);
}
}
}"
```
## 复杂工作流
### 登录并保存认证状态
```bash
playwright-cli run-code "async page => {
await page.goto('https://example.com/login');
await page.fill('input[name=email]', 'user@example.com');
await page.fill('input[name=password]', 'secret');
await page.click('button[type=submit]');
await page.waitForURL('**/dashboard');
await page.context().storageState({ path: 'auth.json' });
return '登录成功,状态已保存至 auth.json';
}"
```
### 从多个页面抓取数据
```bash
playwright-cli run-code "async page => {
const results = [];
for (let i = 1; i <= 5; i++) {
await page.goto(\`https://example.com/products?page=\${i}\`);
await page.waitForSelector('.product-card');
const items = await page.locator('.product-card').evaluateAll(cards =>
cards.map(card => ({
name: card.querySelector('.title')?.textContent?.trim(),
price: card.querySelector('.price')?.textContent?.trim(),
rating: card.querySelector('.rating')?.getAttribute('data-value')
}))
);
results.push(...items);
}
return JSON.stringify(results, null, 2);
}"
```
### 填写多步骤表单(向导)
```bash
playwright-cli run-code "async page => {
// 步骤 1:个人信息
await page.fill('#firstName', 'Jane');
await page.fill('#lastName', 'Doe');
await page.fill('#email', 'jane@example.com');
await page.click('button:text(\"Next\")');
// 步骤 2:地址
await page.waitForSelector('#street');
await page.fill('#street', '123 Main St');
await page.fill('#city', 'Springfield');
await page.selectOption('#state', 'IL');
await page.fill('#zip', '62701');
await page.click('button:text(\"Next\")');
// 步骤 3:确认并提交
await page.waitForSelector('.review-summary');
await page.click('button:text(\"Submit\")');
await page.waitForURL('**/confirmation');
return '表单提交成功';
}"
```
### 无限滚动数据提取
```bash
playwright-cli run-code "async page => {
await page.goto('https://example.com/feed');
const items = [];
while (items.length < 50) {
const newItems = await page.locator('.feed-item').evaluateAll(
els => els.map(el => el.textContent.trim())
);
items.push(...newItems.filter(item => !items.includes(item)));
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1000);
// 检查是否已到达底部
const hasMore = await page.locator('.load-more').isVisible().catch(() => false);
if (!hasMore && newItems.length === items.length) break;
}
return items.slice(0, 50);
}"
```
## 提示
- **返回值**:始终从 `run-code``return` 数据,以便在 CLI 输出中查看
- **`page.context()`**:访问浏览器上下文以操作权限、cookies、存储、地理定位
- **错误信息**Playwright 错误包含元素快照和调用日志——请仔细阅读
- **与 CLI 结合使用**:使用 `run-code` 进行设置(权限、路由),然后使用 CLI 命令进行交互
- **Async/await**:所有 Playwright 操作都是异步的——务必使用 `await`