# 高级工作流 > **适用场景**:复杂的多步骤自动化场景——多页面抓取、弹窗与新窗口处理、无障碍审计、文件下载、OAuth 认证流程、无限滚动提取、表单向导自动化,以及将多个 CLI 功能组合使用。 > **前置知识**:[core-commands.md](core-commands.md)、[running-custom-code.md](running-custom-code.md)、[session-management.md](session-management.md) ## 快速参考 ```bash # 多页面抓取 playwright-cli run-code "async page => { const data = []; for (let i = 1; i <= 5; i++) { await page.goto(\`https://example.com/page/\${i}\`); const items = await page.locator('.item').allTextContents(); data.push(...items); } return data; }" # 处理弹窗 playwright-cli run-code "async page => { const [popup] = await Promise.all([ page.waitForEvent('popup'), page.click('a[target=_blank]') ]); return await popup.title(); }" # 无障碍快照 playwright-cli run-code "async page => { return await page.accessibility.snapshot(); }" ``` ## 弹窗与新窗口处理 当点击的链接会打开新窗口或弹窗时: ### 捕获弹窗内容 ```bash playwright-cli run-code "async page => { const [popup] = await Promise.all([ page.waitForEvent('popup'), page.click('a[target=_blank]') ]); await popup.waitForLoadState(); const title = await popup.title(); const url = popup.url(); return { title, url }; }" ``` ### 与弹窗交互 ```bash playwright-cli run-code "async page => { const [popup] = await Promise.all([ page.waitForEvent('popup'), page.click('#open-settings') ]); // 在弹窗中填写表单 await popup.fill('input[name=email]', 'user@example.com'); await popup.click('button:text(\"Save\")'); // 等待弹窗关闭 await popup.waitForEvent('close'); return 'Popup handled'; }" ``` ### 使用弹窗进行 OAuth 登录 ```bash playwright-cli run-code "async page => { await page.goto('https://app.example.com/login'); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.click('button:text(\"Sign in with Google\")') ]); // 在弹窗中处理 Google OAuth await popup.fill('input[type=email]', 'user@gmail.com'); await popup.click('#identifierNext'); await popup.waitForSelector('input[type=password]', { state: 'visible' }); await popup.fill('input[type=password]', 'password'); await popup.click('#passwordNext'); // 认证后弹窗关闭,主页面跳转 await popup.waitForEvent('close'); await page.waitForURL('**/dashboard'); return 'OAuth login complete: ' + page.url(); }" ``` ## 多页面导航与抓取 ### 分页数据提取 ```bash playwright-cli run-code "async page => { await page.goto('https://example.com/products'); const allProducts = []; while (true) { // 从当前页面提取数据 const products = await page.locator('.product-card').evaluateAll(cards => cards.map(card => ({ name: card.querySelector('.name')?.textContent?.trim(), price: card.querySelector('.price')?.textContent?.trim(), rating: card.querySelector('.rating')?.getAttribute('data-score') })) ); allProducts.push(...products); // 检查是否有下一页 const nextBtn = page.locator('a.next-page'); if (await nextBtn.isVisible()) { await nextBtn.click(); await page.waitForLoadState('networkidle'); } else { break; } } return { total: allProducts.length, products: allProducts }; }" ``` ### 无限滚动提取 ```bash playwright-cli run-code "async page => { await page.goto('https://example.com/feed'); const seen = new Set(); const items = []; const maxItems = 100; while (items.length < maxItems) { // 收集可见条目 const newItems = await page.locator('.feed-item').evaluateAll( els => els.map(el => ({ id: el.getAttribute('data-id'), text: el.textContent.trim() })) ); for (const item of newItems) { if (item.id && !seen.has(item.id)) { seen.add(item.id); items.push(item); } } // 滚动到底部 const previousHeight = await page.evaluate(() => document.body.scrollHeight); await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(2000); // 检查是否已到达底部 const newHeight = await page.evaluate(() => document.body.scrollHeight); if (newHeight === previousHeight) break; } return { collected: items.length, items: items.slice(0, maxItems) }; }" ``` ### 多站点数据聚合 ```bash #!/bin/bash # 使用命名会话实现并发站点抓取 playwright-cli -s=site1 open https://news.example.com & playwright-cli -s=site2 open https://blog.example.com & playwright-cli -s=site3 open https://docs.example.com & wait # 从每个站点提取标题 playwright-cli -s=site1 run-code "async page => { return await page.locator('h2.headline').allTextContents(); }" playwright-cli -s=site2 run-code "async page => { return await page.locator('.post-title').allTextContents(); }" playwright-cli -s=site3 run-code "async page => { return await page.locator('.doc-title').allTextContents(); }" playwright-cli close-all ``` ## 文件上传与下载 ### 下载文件 ```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, url: download.url() }; }" ``` ### 下载多个文件 ```bash playwright-cli run-code "async page => { const downloadLinks = await page.locator('a.download').all(); const files = []; for (const link of downloadLinks) { const [download] = await Promise.all([ page.waitForEvent('download'), link.click() ]); const name = download.suggestedFilename(); await download.saveAs('./downloads/' + name); files.push(name); } return files; }" ``` ### 上传文件 ```bash # 通过元素引用进行简单上传 playwright-cli snapshot playwright-cli upload e5 ./document.pdf # 上传多个文件 playwright-cli upload e5 ./photo1.jpg ./photo2.jpg ./photo3.jpg # 编程式上传(适用于隐藏的文件输入) playwright-cli run-code "async page => { const fileInput = page.locator('input[type=file]'); await fileInput.setInputFiles('./report.pdf'); }" # 编程式上传多个文件 playwright-cli run-code "async page => { const fileInput = page.locator('input[type=file]'); await fileInput.setInputFiles([ './document1.pdf', './document2.pdf', './image.png' ]); }" # 清除文件输入 playwright-cli run-code "async page => { await page.locator('input[type=file]').setInputFiles([]); }" ``` ### 拖放文件上传 ```bash playwright-cli run-code "async page => { // 为拖放区域创建模拟文件 const dataTransfer = await page.evaluateHandle(() => new DataTransfer()); await page.dispatchEvent('.dropzone', 'drop', { dataTransfer }); }" ``` ## 无障碍审计 ### 无障碍树快照 ```bash playwright-cli run-code "async page => { const snapshot = await page.accessibility.snapshot(); return JSON.stringify(snapshot, null, 2); }" ``` ### 检查缺失的 ARIA 标签 ```bash playwright-cli run-code "async page => { return await page.evaluate(() => { const issues = []; // 没有 alt 文本的图片 document.querySelectorAll('img:not([alt])').forEach(img => { issues.push({ type: 'img-no-alt', src: img.src.substring(0, 50) }); }); // 没有无障碍名称的按钮 document.querySelectorAll('button').forEach(btn => { if (!btn.textContent.trim() && !btn.getAttribute('aria-label')) { issues.push({ type: 'button-no-name', html: btn.outerHTML.substring(0, 80) }); } }); // 没有标签的表单输入 document.querySelectorAll('input:not([type=hidden])').forEach(input => { const id = input.id; const hasLabel = id && document.querySelector(\`label[for=\${id}]\`); const hasAriaLabel = input.getAttribute('aria-label') || input.getAttribute('aria-labelledby'); if (!hasLabel && !hasAriaLabel) { issues.push({ type: 'input-no-label', name: input.name, type: input.type }); } }); // 没有文本的链接 document.querySelectorAll('a').forEach(link => { if (!link.textContent.trim() && !link.getAttribute('aria-label')) { issues.push({ type: 'link-no-text', href: link.href.substring(0, 50) }); } }); return { issueCount: issues.length, issues }; }); }" ``` ### 颜色对比度检查 ```bash playwright-cli run-code "async page => { return await page.evaluate(() => { const getContrast = (rgb1, rgb2) => { const luminance = (r, g, b) => { const [rs, gs, bs] = [r, g, b].map(c => { c = c / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs; }; const l1 = luminance(...rgb1); const l2 = luminance(...rgb2); const lighter = Math.max(l1, l2); const darker = Math.min(l1, l2); return (lighter + 0.05) / (darker + 0.05); }; const parseRGB = (str) => { const match = str.match(/\d+/g); return match ? match.slice(0, 3).map(Number) : [0, 0, 0]; }; const lowContrast = []; document.querySelectorAll('p, span, a, button, label, h1, h2, h3, h4, h5, h6').forEach(el => { const style = window.getComputedStyle(el); const fg = parseRGB(style.color); const bg = parseRGB(style.backgroundColor); const ratio = getContrast(fg, bg); if (ratio < 4.5) { lowContrast.push({ text: el.textContent.trim().substring(0, 30), ratio: ratio.toFixed(2), fg: style.color, bg: style.backgroundColor }); } }); return { lowContrastCount: lowContrast.length, elements: lowContrast.slice(0, 20) }; }); }" ``` ### Tab 键顺序验证 ```bash playwright-cli run-code "async page => { return await page.evaluate(() => { const focusable = Array.from(document.querySelectorAll( 'a[href], button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])' )); return focusable.map((el, i) => ({ order: i + 1, tag: el.tagName.toLowerCase(), text: (el.textContent || el.getAttribute('aria-label') || el.name || '').trim().substring(0, 40), tabIndex: el.tabIndex })); }); }" ``` ## 表单向导自动化 ### 多步骤表单 ```bash # 步骤 1:个人信息 playwright-cli open https://example.com/apply playwright-cli snapshot playwright-cli fill e1 "Jane" # 名 playwright-cli fill e2 "Doe" # 姓 playwright-cli fill e3 "jane@example.com" # 邮箱 playwright-cli fill e4 "+1-555-0123" # 电话 playwright-cli click e5 # 下一步 # 步骤 2:地址 playwright-cli snapshot playwright-cli fill e1 "123 Main Street" playwright-cli fill e2 "Apt 4B" playwright-cli fill e3 "Springfield" playwright-cli select e4 "IL" playwright-cli fill e5 "62701" playwright-cli click e6 # 下一步 # 步骤 3:上传文档 playwright-cli snapshot playwright-cli upload e1 ./resume.pdf playwright-cli upload e2 ./cover-letter.pdf playwright-cli click e3 # 下一步 # 步骤 4:审核与提交 playwright-cli snapshot playwright-cli screenshot --filename=review-page.png playwright-cli check e1 # 接受条款 playwright-cli click e2 # 提交 playwright-cli snapshot # 确认页面 ``` ### 带条件字段的动态表单 ```bash playwright-cli run-code "async page => { await page.goto('https://example.com/registration'); // 选择账户类型——这会显示/隐藏字段 await page.selectOption('#account-type', 'business'); // 等待企业字段出现 await page.waitForSelector('#company-name', { state: 'visible' }); // 填写企业专属字段 await page.fill('#company-name', 'Acme Corp'); await page.fill('#tax-id', '12-3456789'); await page.fill('#company-size', '50-100'); // 填写通用字段 await page.fill('#contact-name', 'Jane Doe'); await page.fill('#contact-email', 'jane@acme.com'); await page.click('button:text(\"Register\")'); await page.waitForURL('**/welcome'); return 'Registration complete'; }" ``` ## 数据提取模式 ### 表格数据提取 ```bash playwright-cli run-code "async page => { await page.goto('https://example.com/reports'); const data = await page.evaluate(() => { const table = document.querySelector('table.data-table'); const headers = Array.from(table.querySelectorAll('thead th')) .map(th => th.textContent.trim()); const rows = Array.from(table.querySelectorAll('tbody tr')) .map(tr => { const cells = Array.from(tr.querySelectorAll('td')) .map(td => td.textContent.trim()); return Object.fromEntries(headers.map((h, i) => [h, cells[i]])); }); return { headers, rowCount: rows.length, rows }; }); return JSON.stringify(data, null, 2); }" ``` ### 提取结构化数据(JSON-LD、Meta 标签) ```bash playwright-cli run-code "async page => { return await page.evaluate(() => { // JSON-LD 结构化数据 const jsonLd = Array.from(document.querySelectorAll('script[type=\"application/ld+json\"]')) .map(s => JSON.parse(s.textContent)); // Open Graph meta 标签 const og = {}; document.querySelectorAll('meta[property^=\"og:\"]').forEach(m => { og[m.getAttribute('property')] = m.content; }); // 标准 meta 标签 const meta = {}; document.querySelectorAll('meta[name]').forEach(m => { meta[m.name] = m.content; }); return { jsonLd, openGraph: og, meta }; }); }" ``` ### 截图每个链接的目标页面 ```bash playwright-cli run-code "async page => { await page.goto('https://example.com'); const links = await page.locator('nav a').evaluateAll( anchors => anchors.map(a => ({ text: a.textContent.trim(), href: a.href })) ); for (const link of links) { const safeName = link.text.toLowerCase().replace(/[^a-z0-9]/g, '-'); await page.goto(link.href); await page.waitForLoadState('networkidle'); await page.screenshot({ path: \`screenshots/\${safeName}.png\` }); } return links.map(l => l.text); }" ``` ## 错误恢复模式 ### 失败时重试 ```bash playwright-cli run-code "async page => { const maxRetries = 3; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { await page.goto('https://flaky-site.example.com', { timeout: 15000 }); await page.waitForSelector('.content', { timeout: 5000 }); return 'Success on attempt ' + attempt; } catch (error) { if (attempt === maxRetries) throw error; console.log(\`Attempt \${attempt} failed, retrying...\`); await page.waitForTimeout(2000 * attempt); // 指数退避 } } }" ``` ### 自动关闭 Cookie 横幅 ```bash playwright-cli run-code "async page => { // 尝试常见的 Cookie 同意选择器 const selectors = [ 'button:text(\"Accept\")', 'button:text(\"Accept All\")', 'button:text(\"Accept Cookies\")', 'button:text(\"I Agree\")', '#cookie-accept', '.cookie-consent-accept', '[data-testid=cookie-accept]' ]; for (const sel of selectors) { try { const btn = page.locator(sel).first(); if (await btn.isVisible({ timeout: 1000 })) { await btn.click(); return 'Dismissed cookie banner with: ' + sel; } } catch (e) { // 尝试下一个选择器 } } return 'No cookie banner found'; }" ``` ### 处理意外弹框 ```bash # 设置自动关闭所有弹框 playwright-cli run-code "async page => { page.on('dialog', async dialog => { console.log(\`Auto-handled \${dialog.type()} dialog: \${dialog.message()}\`); await dialog.dismiss(); }); }" ``` ## 组合 CLI 功能 ### 完整的端到端测试工作流 ```bash #!/bin/bash set -e # 1. 启动新的会话 playwright-cli open https://app.example.com --persistent # 2. 设置监控 playwright-cli run-code "async page => { page.on('console', msg => { if (msg.type() === 'error') console.log('[ERROR]', msg.text()); }); }" # 3. 启动追踪 playwright-cli tracing-start # 4. 登录 playwright-cli goto https://app.example.com/login playwright-cli snapshot playwright-cli fill e1 "test@example.com" playwright-cli fill e2 "password123" playwright-cli click e3 playwright-cli state-save auth-state.json # 5. 验证仪表盘 playwright-cli snapshot playwright-cli screenshot --filename=dashboard.png # 6. 测试某个功能 playwright-cli click e5 # 导航至功能页面 playwright-cli snapshot playwright-cli fill e1 "test data" playwright-cli click e3 # 提交 playwright-cli screenshot --filename=feature-result.png # 7. 停止追踪并清理 playwright-cli tracing-stop playwright-cli close echo "Test complete. Check screenshots/ and traces/" ``` ### 对比测试脚本 ```bash #!/bin/bash # 对比两个环境 STAGING="https://staging.example.com" PROD="https://www.example.com" PAGES=("/" "/pricing" "/about" "/features") for path in "${PAGES[@]}"; do safePath=$(echo "$path" | tr '/' '-' | sed 's/^-//') [ -z "$safePath" ] && safePath="home" playwright-cli -s=staging open "${STAGING}${path}" playwright-cli -s=staging screenshot --filename="compare-staging-${safePath}.png" playwright-cli -s=prod open "${PROD}${path}" playwright-cli -s=prod screenshot --filename="compare-prod-${safePath}.png" done playwright-cli close-all echo "Screenshots saved. Compare staging vs prod visually." ``` ## 提示 - **从简到繁**——从基本的 CLI 命令开始,仅在必要时使用 `run-code` - **保存状态检查点**——在关键节点执行 `state-save checkpoint.json`,以便出问题时可以恢复 - **使用命名会话实现隔离**——切勿在单个会话中混杂不同任务 - **监控控制台和网络**——尽早设置监听器,以便实时捕获错误 - **追踪复杂流程**——调试多步骤工作流时始终开启追踪 - **错误恢复至关重要**——网站不稳定,请在复杂脚本中构建重试逻辑 - **清理资源**——每个脚本末尾执行 `close-all`;出问题时执行 `kill-all`