name: playwright-debugging description: 当 Playwright 脚本失败、测试不稳定、选择器停止工作或超时时使用 - 提供浏览器自动化问题的系统化调试方法 user-invocable: false
Playwright 调试
概述
浏览器自动化故障可归为可预测的类别。本技能提供系统化的方法,快速诊断和修复问题。
何时使用
- 之前工作的脚本现在失败
- 间歇性测试失败(不稳定性)
- “元素未找到”错误
- 超时错误
- 自动化中的意外行为
- 元素不可交互
何时不使用:
- 编写新自动化(使用 playwright-patterns 技能)
- API 或后端调试
快速参考
| 问题 | 首选操作 |
|---|---|
| 定位器超时 | 使用 --ui 模式运行,使用 .count()、.isVisible() 检查元素状态 |
| 不稳定测试(有时通过) | 用基于条件的等待替换 waitForTimeout() |
| “元素不可见” | 检查计算样式,等待覆盖层消失 |
| 本地工作,CI 失败 | 使用 waitForLoadState('networkidle'),增加超时 |
| 元素不可点击 | 检查是否被覆盖层覆盖,等待动画完成 |
| 陈旧元素 | 导航后重新查询,而不是存储定位器 |
诊断框架
1. 复现和隔离
第一步:你能复现吗?
// 运行单个测试以隔离问题
npx playwright test path/to/test.spec.js
// 以有头模式运行以观察
npx playwright test --headed
// 以慢动作运行
npx playwright test --headed --slow-mo=1000
需回答的问题:
- 失败是否一致或间歇性?
- 在所有浏览器中失败还是仅一种?
- 在有头和无头模式中都失败吗?
- 最近有变化吗(网站更新、代码更改)?
2. 增加可见性
使用 UI 模式进行交互式调试:
# 最适合本地开发 - 提供时间旅行调试
npx playwright test --ui
UI 模式提供:
- 所有操作的可视时间线
- 文件更改时的观察模式
- 网络和控制台标签
- 测试执行的时间旅行
使用检查器逐步执行测试:
# 通过实时浏览器逐步执行测试
npx playwright test --debug
检查器允许:
- 逐步执行操作
- 直接从浏览器拾取定位器
- 实时编辑选择器并查看结果
- 查看可操作性日志
在失败点截图:
// 在失败操作前
await page.screenshot({ path: 'before-action.png', fullPage: true });
// 尝试操作
try {
await page.click('.button');
} catch (error) {
await page.screenshot({ path: 'after-error.png', fullPage: true });
throw error;
}
启用详细日志:
# API 级别调试
DEBUG=pw:api npx playwright test
# 带有 playwright 对象的浏览器 DevTools
PWDEBUG=console npx playwright test
使用 PWDEBUG=console,你可以在 DevTools 中访问:
// 在浏览器控制台中
playwright.$('.selector') // 使用 Playwright 引擎查询
playwright.$$('selector') // 获取所有匹配项
playwright.inspect('selector') // 在元素面板中高亮
playwright.locator('selector') // 创建定位器
使用跟踪查看器:
// 记录跟踪
await context.tracing.start({ screenshots: true, snapshots: true });
// ... 你的测试代码
await context.tracing.stop({ path: 'trace.zip' });
// 查看跟踪
npx playwright show-trace trace.zip
使用测试步骤组织跟踪:
// 在跟踪查看器中分组操作
await test.step('登录', async () => {
await page.fill('input[name="username"]', '用户');
await page.click('button[type="submit"]');
});
await test.step('导航到仪表板', async () => {
await page.click('a[href="/dashboard"]');
});
为定位器添加描述以提高清晰度:
// 描述出现在跟踪查看器和报告中
const submitButton = page.locator('#submit').describe('提交按钮');
await submitButton.click();
VS Code 调试:
安装 Playwright VS Code 扩展以:
- 在 VS Code 中使用断点进行实时调试
- 编辑时在浏览器中高亮定位器
- “显示浏览器”选项以获得实时反馈
- 右键单击任何测试进行“调试测试”
这将调试直接集成到你的编辑器工作流程中。
3. 检查元素状态
检查元素是否存在:
const element = page.locator('.button');
// 是否在 DOM 中存在?
const count = await element.count();
console.log(`找到 ${count} 个元素`);
// 是否可见?
const isVisible = await element.isVisible();
console.log(`可见:${isVisible}`);
// 是否启用?
const isEnabled = await element.isEnabled();
console.log(`启用:${isEnabled}`);
// 获取所有属性
const attrs = await element.evaluate(el => ({
classes: el.className,
id: el.id,
display: window.getComputedStyle(el).display,
visibility: window.getComputedStyle(el).visibility,
opacity: window.getComputedStyle(el).opacity
}));
console.log(attrs);
4. 验证选择器
在浏览器控制台中测试选择器:
// 使用 page.evaluate 测试选择器
const found = await page.evaluate(() => {
const el = document.querySelector('.button');
return el ? {
text: el.textContent,
visible: el.offsetParent !== null,
enabled: !el.disabled
} : null;
});
console.log('选择器测试:', found);
检查多个匹配项:
// 有多个元素吗?
const all = await page.locator('.button').all();
console.log(`找到 ${all.length} 个匹配元素`);
// 获取所有匹配项的文本
const texts = await page.locator('.button').allTextContents();
console.log('所有匹配文本:', texts);
常见问题和修复
问题:元素未找到
原因:
- 选择器错误
- 元素尚未加载
- 元素在 iframe 中
- 元素是动态创建的
调试步骤:
// 1. 检查选择器是否存在
const exists = await page.locator('.button').count() > 0;
console.log('元素存在:', exists);
// 2. 显式等待元素(现代方法)
await page.locator('.button').waitFor({ timeout: 10000 });
// 或让自动等待处理:
await page.locator('.button').click();
// 3. 检查是否在 iframe 中
const frame = page.frameLocator('iframe');
await frame.locator('.button').click();
// 4. 转储所有匹配元素
const all = await page.evaluate(() => {
return Array.from(document.querySelectorAll('button')).map(el => ({
text: el.textContent,
classes: el.className,
id: el.id
}));
});
console.log('页面上的所有按钮:', all);
问题:元素不可见/不可点击
原因:
- 元素被隐藏(CSS: display:none, visibility:hidden)
- 元素被另一个元素覆盖
- 元素在视口外
- 元素动画未完成
调试步骤:
// 1. 检查计算样式
const styles = await page.locator('.button').evaluate(el => ({
display: window.getComputedStyle(el).display,
visibility: window.getComputedStyle(el).visibility,
opacity: window.getComputedStyle(el).opacity,
zIndex: window.getComputedStyle(el).zIndex
}));
console.log('元素样式:', styles);
// 2. 滚动到视图中
await page.locator('.button').scrollIntoViewIfNeeded();
// 3. 等待元素稳定(不处于动画中)
await expect(page.locator('.button')).toBeVisible();
await page.waitForTimeout(100); // 短暂等待动画
// 4. 如需,强制点击(最后手段)
await page.locator('.button').click({ force: true });
问题:时序/竞态条件
原因:
- 网络请求未完成
- JavaScript 仍在执行
- 动画进行中
- 动态内容加载
调试步骤:
// 1. 等待网络空闲
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');
// 2. 等待特定网络请求
await page.waitForResponse(resp =>
resp.url().includes('/api/data') && resp.status() === 200
);
// 3. 等待 JavaScript 条件
await page.waitForFunction(() =>
window.dataLoaded === true
);
// 4. 等待元素计数稳定
await expect(page.locator('.item')).toHaveCount(10);
问题:陈旧元素引用
原因:
- 页面刷新或导航
- 元素从 DOM 中移除并重新添加
- 动态内容替换了元素
修复:
// 不要在导航过程中存储元素句柄
const button = page.locator('.button'); // 错误:可能变陈旧
await page.goto('/other-page');
await button.click(); // 错误:陈旧
// 在导航后重新查询
await page.goto('/other-page');
await page.locator('.button').click(); // 正确:新鲜查询
问题:表单提交不工作
原因:
- JavaScript 验证阻止提交
- 事件监听器尚未附加
- 表单操作未正确设置
调试步骤:
// 1. 提交前验证表单状态
const formState = await page.evaluate(() => {
const form = document.querySelector('form');
return {
action: form?.action,
method: form?.method,
valid: form?.checkValidity()
};
});
console.log('表单状态:', formState);
// 2. 手动触发表单事件
await page.fill('input[name="email"]', 'test@example.com');
await page.dispatchEvent('input[name="email"]', 'blur');
// 3. 使用 form.submit() 而不是点击按钮
await page.evaluate(() => document.querySelector('form').submit());
常见错误
| 错误 | 为什么错误 | 正确方法 |
|---|---|---|
添加 waitForTimeout(5000) |
掩盖时序问题,使测试变慢,不可靠 | 使用基于条件的等待:expect().toBeVisible() |
| 在未理解原因的情况下强制点击 | 绕过 Playwright 的可操作性检查 | 诊断元素不可点击的原因,修复根本原因 |
| 不使用现代调试工具 | 诊断缓慢,猜测问题 | 从 --ui 或 --debug 开始进行可视化调试 |
| 仅在有头模式下测试 | 隐藏 CI 中出现的时序问题 | 始终在无头模式下测试 |
| 使用脆弱的选择器 | HTML 结构更改时中断 | 使用基于角色或 data-testid 的选择器 |
| 跳过跟踪查看器 | 错过发生的详细时间线 | 为失败测试启用跟踪 |
调试检查清单
自动化失败时,按顺序检查:
- ☐ 我能一致复现失败吗?
- ☐ 在有头模式和慢动作下失败吗?
- ☐ 我是否在失败前后截图了?
- ☐ 选择器是否实际匹配元素?
- ☐ 元素是否可见和启用?
- ☐ 元素是否在 iframe 中?
- ☐ 我是否等待了页面加载完成?
- ☐ 是否有需要时间加载的动态内容?
- ☐ 是否有网络请求仍在进行?
- ☐ 我是否检查了浏览器控制台的 JavaScript 错误?
调试工具参考
| 工具 | 命令 | 使用时机 |
|---|---|---|
| UI 模式 | --ui |
时间旅行调试,带可视时间线(最适合本地开发) |
| 检查器 | --debug |
逐步执行测试,实时拾取定位器 |
| 有头模式 | --headed |
需要查看浏览器 |
| 慢动作 | --slow-mo=1000 |
动作太快无法观察 |
| 调试模式 | PWDEBUG=1 |
打开检查器(旧方法,优先使用 --debug) |
| 控制台调试 | PWDEBUG=console |
使用 playwright 对象访问浏览器 DevTools |
| 跟踪查看器 | show-trace trace.zip |
需要完整时间线分析 |
| 截图 | page.screenshot() |
需要视觉证据 |
| 控制台日志 | DEBUG=pw:api |
需要 API 调用详情 |
| 暂停 | await page.pause() |
需要手动检查 |
不稳定模式
不稳定:80% 的时间工作
可能原因: 竞态条件
修复:
// 替换任意等待
await page.waitForTimeout(2000); // 错误
// 使用基于条件的等待
await expect(page.locator('.result')).toBeVisible(); // 正确
不稳定:CI 失败,本地工作
可能原因: 时序差异
修复:
// 为 CI 增加默认超时
test.setTimeout(60000);
page.setDefaultTimeout(30000);
// 等待网络空闲
await page.waitForLoadState('networkidle');
不稳定:失败并显示“元素不可点击”
可能原因: 重叠元素或动画
修复:
// 等待元素可操作
await expect(page.locator('.button')).toBeVisible();
await expect(page.locator('.button')).toBeEnabled();
// 或等待覆盖层消失
await expect(page.locator('.loading-overlay')).not.toBeVisible();
记住
调试优先级:
- 可靠复现问题
- 增加可见性(截图、日志、跟踪)
- 验证元素状态和选择器
- 检查时序和等待
- 在不同模式(有头、浏览器)中测试
自动等待优势: Playwright 自动等待元素:
- 附加到 DOM
- 可见
- 启用且稳定
- 不被覆盖层覆盖
大多数操作(点击、填充等)包括自动等待。显式等待仅需用于复杂条件。
大多数 Playwright 问题是时序相关的。用基于条件的等待替换任意超时。如有疑问,减速并在有头模式下使用 --ui 或 --debug 观察。