15 KiB
15 KiB
运行自定义 Playwright 代码
何时使用:当 CLI 命令不够用时——地理定位、权限、媒体模拟、等待策略、iframe 交互、文件下载、剪贴板访问、复杂的多步骤工作流,或任何需要完整 Playwright API 的场景。 前置条件:core-commands.md 了解基本的 CLI 用法
快速参考
# 语法:run-code 接受一个以 page 为参数的 async 函数
playwright-cli run-code "async page => {
// 此处可使用完整的 Playwright API
// page.context() 用于浏览器上下文操作
// 返回一个值即可在输出中查看
return await page.title();
}"
地理定位
覆盖浏览器报告的位置——对于测试基于位置的功能(如门店查找器、配送区域和天气应用)至关重要。
# 授予权限并将位置设置为纽约
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();
}"
权限
控制浏览器权限授予——通知、摄像头、麦克风、剪贴板等。
# 授予多个权限
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
媒体模拟
测试你的应用在不同媒体条件下的表现——深色模式、减弱动效、打印布局。
色彩方案
# 深色模式
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' });
}"
减弱动效
# 模拟 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' });
}"
强制色彩(高对比度)
playwright-cli run-code "async page => {
await page.emulateMedia({ forcedColors: 'active' });
}"
打印媒体
# 模拟打印样式表(在生成 PDF 前使用)
playwright-cli run-code "async page => {
await page.emulateMedia({ media: 'print' });
}"
# 重置为屏幕模式
playwright-cli run-code "async page => {
await page.emulateMedia({ media: 'screen' });
}"
组合多个模拟
playwright-cli run-code "async page => {
await page.emulateMedia({
colorScheme: 'dark',
reducedMotion: 'reduce',
forcedColors: 'none'
});
}"
区域设置和时区
覆盖浏览器区域设置和时区以测试国际化:
# 设置区域设置(影响数字格式、日期显示等)
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" }
等待策略
当页面在操作后需要时间加载、渲染或稳定时使用。
等待加载状态
# 等待所有网络请求完成
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');
}"
等待元素
# 等待加载旋转器消失
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 变化
playwright-cli run-code "async page => {
await page.waitForURL('**/dashboard');
}"
playwright-cli run-code "async page => {
await page.waitForURL(/.*\/order\/\d+/);
}"
等待自定义条件
# 等待 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 }
);
}"
等待网络请求
# 等待特定 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 内部的内容交互:
# 点击 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();
}"
文件下载
# 触发下载并保存文件
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();
}"
剪贴板
# 读取剪贴板内容
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;
}"
页面信息
# 页面标题
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 执行
# 执行并返回结果
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%';
});
}"
错误处理
# 对可选元素使用 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);
}
}
}"
复杂工作流
登录并保存认证状态
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';
}"
从多个页面抓取数据
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);
}"
填写多步骤表单(向导)
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 '表单提交成功';
}"
无限滚动数据提取
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