18 KiB
18 KiB
高级工作流
适用场景:复杂的多步骤自动化场景——多页面抓取、弹窗与新窗口处理、无障碍审计、文件下载、OAuth 认证流程、无限滚动提取、表单向导自动化,以及将多个 CLI 功能组合使用。 前置知识:core-commands.md、running-custom-code.md、session-management.md
快速参考
# 多页面抓取
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();
}"
弹窗与新窗口处理
当点击的链接会打开新窗口或弹窗时:
捕获弹窗内容
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 };
}"
与弹窗交互
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 登录
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();
}"
多页面导航与抓取
分页数据提取
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 };
}"
无限滚动提取
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) };
}"
多站点数据聚合
#!/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
文件上传与下载
下载文件
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()
};
}"
下载多个文件
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;
}"
上传文件
# 通过元素引用进行简单上传
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([]);
}"
拖放文件上传
playwright-cli run-code "async page => {
// 为拖放区域创建模拟文件
const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
await page.dispatchEvent('.dropzone', 'drop', { dataTransfer });
}"
无障碍审计
无障碍树快照
playwright-cli run-code "async page => {
const snapshot = await page.accessibility.snapshot();
return JSON.stringify(snapshot, null, 2);
}"
检查缺失的 ARIA 标签
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 };
});
}"
颜色对比度检查
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 键顺序验证
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
}));
});
}"
表单向导自动化
多步骤表单
# 步骤 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 # 确认页面
带条件字段的动态表单
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';
}"
数据提取模式
表格数据提取
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 标签)
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 };
});
}"
截图每个链接的目标页面
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);
}"
错误恢复模式
失败时重试
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 横幅
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';
}"
处理意外弹框
# 设置自动关闭所有弹框
playwright-cli run-code "async page => {
page.on('dialog', async dialog => {
console.log(\`Auto-handled \${dialog.type()} dialog: \${dialog.message()}\`);
await dialog.dismiss();
});
}"
组合 CLI 功能
完整的端到端测试工作流
#!/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/"
对比测试脚本
#!/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