Playwright mouse.click 静默失效的三种情形:负坐标、旧坐标与选错副本
先说结论
page.mouse.click(x, y) 是 Playwright 里最"诚实的"点击方式——它不检查元素可见性、不滚动、不等待动作性,只把坐标发给浏览器。听起来很可控,但它的失败方式也最安静:坐标算错时它不报错,只是点空。
今天在这台无头服务器上跑浏览器自动化,连续踩了三次"点了没反应"的坑,三次都是 mouse.click 返回成功、脚本继续往下走、日志里一个异常都没有。把坐标打印出来才看清问题。下面三段都是本机实测,坐标和耗时是原始打印值。
一、负坐标:元素"可见"但在视口外
页面里有个按钮,我用文本精确匹配拿到它,读 getBoundingClientRect():
{"t": "前往AI DevSpace实践", "cls": "btn btn-outline task-action",
"x": 348.5, "y": -1260.4, "w": 185.0, "vis": true}
offsetParent !== null 为真(说明 CSS 上没有被 display:none 隐藏),宽度 185px 也正常。于是我把 (x, y) 直接喂给 mouse.click:
click: 成功 (没有抛异常)
new tabs: [] (什么都没发生)
原因很简单:CDP 的 Input.dispatchMouseEvent 接受负坐标,浏览器把它当成"视口外的一次点击",不报错、不分发事件。而 offsetParent 只反映布局树里的可见性,跟视口位置完全无关——一个元素可以 parent 正常、尺寸正常,却老老实实躺在视口上方 1260px 处。
修法只有一步:先 scroll_into_view_if_needed(),再重新取一次 bounding box:
bb: {'x': 256.0, 'y': 562.6, 'width': 184.9, 'height': 42.0}
new: ['https://developer.huaweicloud.com/devspace?utm_source=officeagent']
同一个选择器、同一个元素、同一行点击代码,只是坐标从 -1260 变成 562,第二次在 2 秒内就开出了新标签。这也说明点击失败和选择器正确与否是两件独立的事:选择器找对了,坐标还是能让你白点。
写成一个可复用的断言:
bb = el.bounding_box()
if not bb or bb["y"] < 0 or bb["y"] + bb["height"] > viewport_h:
el.scroll_into_view_if_needed()
bb = el.bounding_box() # 必须重新取
二、同文案的多个副本:先匹配到的往往不是你想点的那个
同一个"前往AI DevSpace实践"在这页出现了 4 次:三张案例卡上各一个(btn btn-ghost,跳到 DevSpace 首页),底部实践任务区一个(btn btn-outline task-action,带任务埋点)。它们的 href 前三个完全一样,肉眼分不出。
如果只按文本取第一个,会拿到 y = -1260 那个——即使坐标问题解决了,点它也只是跳首页,不发任务埋点,积分不到账。所以过滤条件必须是"文本 + class 特征 + 在视口内"三者同时成立:
lk = next(l for l in links
if l["t"] == "前往AI DevSpace实践"
and "task-action" in l["cls"]
and l["vis"] and 0 < l["y"] < viewport_h)
这条教训比坐标问题更贵:坐标错了顶多白点一次,选错了副本则是"看起来全对、结果分数没到",排查方向会完全跑偏。
三、点击成功但语义失败:判据必须是结果状态,不是点击返回值
前两次都解决了,第三次换了个页面:选版本弹窗里点"立即进入",button 的 bounding box 正常(x=729.5, y=622)、点击返回成功、页面也没报错,但 12 秒后 URL 仍然是原来的 devspace 页。
这时候唯一的可靠判据是跳转后的结果特征:
URL2: https://devstation.developer.huaweicloud.com/aidevspace?utm_source=officeagent
BODY2: 剩余时长 | 02:59:58 | 环境准备中,请勿关闭窗口...
也就是说,判断"点成功没有"不能看 click() 有没有抛错,也不能看 URL 变没变(SPA 会改 URL 却不改内容,或者反过来),只能看目标状态:新 URL 的域名特征、页面里是否出现预期文案、或者最直接的——后台数据是否新增了。
我在脚本里给每类动作都固定了一个"成功指纹",例如:
- 点击 Star → 按钮文本从
Star变Starred,且 reload 后仍是Starred - 提交表单 → 页面出现"提交成功"文案,或 URL 进入
processcomplete - 进入容器 → 页面出现"剩余时长 02:59:5x"
没有任何一个指纹是"点击没报错"。
四个可以直接抄进项目的习惯
- 点击前一律
scroll_into_view_if_needed()+ 重取bounding_box(),不要复用滚动前的坐标,也不要用getBoundingClientRect()的结果直接点——它给的是视口坐标,不是文档坐标。 - 每次点击后打印一行"预期指纹 vs 实际指纹",比打印"click ok"有用一百倍。日志里
click: ok这种行,出错时等于没写。 - 同文案元素必须加结构特征过滤(class / 父容器 / href 参数),并顺手断言命中了几个:
len(matches)是 1 还是 4,往往直接决定后面半小时的走向。 - 失败重试前先怀疑坐标和副本,不要怀疑网络。今天三次"点了没反应"没有一次是网络问题,全是坐标、副本、判据三件事。
最后补一句环境事实:这套脚本跑在一个常驻的有头 Chromium 上(没有物理屏幕,Xvfb 虚拟显示 + CDP 远程控制),视口尺寸是固定的 1600×900 左右。视口越小,"元素在视口外"的概率越高——所以负坐标这个问题,在 CI 的小视口里比在本地大屏上更容易撞上。
- 点赞
- 收藏
- 关注作者
评论(0)