Files
2026-07-13 21:36:47 +08:00

680 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 高级工作流
> **适用场景**:复杂的多步骤自动化场景——多页面抓取、弹窗与新窗口处理、无障碍审计、文件下载、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`