CI 截图里“保存”按钮已经出现,locator.click()却在 30 秒后超时。继续加waitForTimeout(5000)有时能绿,但负载一高又失败。这个现象通常不是单纯的“页面慢”,而是元素在可见、稳定、无遮挡、可接收事件等检查中仍有一项不成立。
下面把问题压缩成一个能本地复现的页面,并用 trace 与业务断言定位失败层。示例未在你的环境执行,文中只给出预期观察方式。
一个会移动并被遮罩的按钮
保存为/tmp/actionability.html:
<!doctypehtml><metacharset="utf-8"><style>#save{position:absolute;top:80px;left:10px;transition:left .8s;}#mask{position:fixed;inset:0;background:#fff8;z-index:2;}</style><buttonid="save">保存</button><divid="mask"></div><pid="status"></p><script>save.onclick=()=>status.textContent="saved";requestAnimationFrame(()=>save.style.left="240px");setTimeout(()=>mask.remove(),1200);</script>测试用 Locator 表达目标,用结果文本表达业务成功:
import{test,expect}from'@playwright/test';test('保存动作完成',async({page})=>{awaitpage.goto('file:///tmp/actionability.html');constsave=page.getByRole('button',{name:'保存'});awaitsave.click();awaitexpect(page.locator('#status')).toHaveText('saved');});Playwright 在每次动作前重新解析 Locator,不会把创建 Locator 时的 DOM 节点永久缓存。按钮移动时稳定性检查未通过,遮罩存在时事件接收检查未通过;条件恢复后,动作才会继续。
click 成功只证明动作发出,不证明保存完成
自动等待解决的是动作可执行性,不是业务正确性。按钮可点击后,后端仍可能返回 500,前端也可能吞掉异常。把“能点”和“保存成功”写成两层证据:
constresponsePromise=page.waitForResponse(response=>response.url().endsWith('/api/profile')&&response.request().method()==='PUT');awaitpage.getByRole('button',{name:'保存'}).click();constresponse=awaitresponsePromise;expect(response.status()).toBe(200);awaitexpect(page.getByRole('status')).toContainText('保存成功');不要无条件使用networkidle代替具体响应。埋点、长轮询或 WebSocket 会让“网络空闲”与业务结束无关。验证失败时要区分:动作超时、响应状态错误、响应正确但 UI 没更新,这三类问题属于不同边界。
用 trace 回放最后一次不满足的条件
配置只在失败时保留证据:
import{defineConfig}from'@playwright/test';exportdefaultdefineConfig({timeout:30_000,use:{trace:'retain-on-failure',screenshot:'only-on-failure'}});执行与打开报告:
npx playwrighttestsave.spec.ts--reporter=linetest_status=$?findtest-results-nametrace.zip-printexit"$test_status"实际运行时,测试退出码为 0 且业务断言成立才是成功;非零退出码是失败。若生成trace.zip,可用npx playwright show-trace 路径检查动作前后的 DOM 快照、调用日志与网络事件。Trace 是失败证据,不应因为“重试后通过”而被删除。
针对原因修复,别统一增加休眠
如果日志显示 strict mode violation,应缩小 Locator 直到只匹配一个元素;节点频繁重建时,避免保存elementHandle,继续使用语义 Locator;遮罩拦截事件时,应等待遮罩消失或修复页面状态;业务请求慢,则等待具体响应并为该步骤设置合理超时。
强制点击click({ force: true })会跳过部分检查,适合极少数明确需要绕过用户交互约束的场景,却可能把真实遮挡变成假通过。稳定的 E2E 用例应该留下四段清楚的因果链:目标唯一、动作条件成立、请求得到预期响应、用户可见结果出现。