# 运行自定义 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`