554 lines
15 KiB
Markdown
554 lines
15 KiB
Markdown
# 运行自定义 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`
|