浏览器自动化中的「假成功」:一次点击没生效的五种排查路径
做过前端自动化的人大概都遇到过这种情形:脚本一路没有报错,日志里打印着「点击完成」「表单已提交」,可业务侧的状态一个字都没变。这时候最危险的做法是增加重试次数——因为它不是偶发的抖动,而是我们对「成功」的判定标准本身错了。
下面五条排查路径,来自把浏览器自动化真正放进日常运维流程之后踩出来的经验,每一条都对应一类可复现的「假成功」。
一、先怀疑 DOM 有隐藏副本
现代前端为了适配移动端,经常把同一个操作按钮渲染两份:一份桌面版,一份移动版靠 CSS 隐藏。两份的 class 完全一样,选择器拿到的是数组,很多人顺手取 `[0]`,恰好就是那份隐藏的移动端副本。
隐藏元素的 `getBoundingClientRect().width` 是 0,向它的中心坐标派发鼠标事件不会抛异常,浏览器也「接受」了这个事件,只是什么都没发生。于是脚本认为点过了。
稳健写法是先按可见性过滤,再取第一个:
const btn = [...document.querySelectorAll('.repo-action-group--star')]
.filter(e => e.getBoundingClientRect().width > 0)[0];
判断成功的依据也不能是「点了没报错」,而应是对比操作前后的可观测文本或计数:`Star536` 变成 `Starred537` 才算数。
二、用 elementFromPoint 确认坐标真的落在目标上
坐标点击的第二个陷阱是遮挡。悬浮导航、Cookie 提示条、右下角的客服气泡,都会在视觉上不显眼地压住目标区域。脚本按目标元素的 rect 算出中心点,事件却被上层元素接走了。
在派发鼠标事件之前,用一行代码就能自检:
const hit = document.elementFromPoint(cx, cy);
// 期望:hit.closest('a[href*="目标链接"]') 为真
如果命中的是别的元素,先 `scrollIntoView({block:'center'})` 把目标挪到视口中间,再重新测量坐标。注意不要在平滑滚动尚未结束时测量——`behavior:'smooth'` 的惯性会让坐标全部偏移。
三、合成事件唤不醒框架的监听器
直接给 `input.value = 'xxx'` 赋值,React、Vue 这些框架是收不到通知的:它们在原型上劫持了 value 的 setter,直接赋值绕过了劫持,组件内部状态不会更新。表现就是输入框里明明有字,提交后后端收到的却是空值。
正确做法是走原生 setter,再补发事件:
const setNative = (el, val) => {
const proto = el.tagName === 'TEXTAREA'
? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, val);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
带联想候选的标签输入框更严格:它监听的是真实键盘事件,而不是 `input` 事件。必须逐字派发 `keyDown`(带上 `text` 与 `unmodifiedText`)加 `keyUp`,候选下拉才会出现;`insertText` 这类批量注入对它无效。
四、埋点归属:动作做了,但不属于这个账号
这一类最难查,因为页面看起来完全正常。典型场景是自动化工具新开标签时落到了另一个浏览器上下文——Cookie 是空的,页面显示未登录,点按钮不响应,但脚本也没拿到任何错误。更隐蔽的是跨域 iframe:埋点由 iframe 通过 `postMessage` 上报给父页面,如果你把 iframe 的地址单独在新标签里打开跑完整流程,父页面根本不存在,服务端自然收不到这条行为记录。
排查顺序建议是:先确认新标签与已登录页面处于同一个浏览器上下文(CDP 里即 `browserContextId`),再确认交互发生在父页面的 iframe 上下文内,而不是独立标签。
五、读到的状态可能是上一次渲染的快照
最后一条不是点击问题,而是读的问题。页面 DOM 不会自动跟着服务端变化,`innerText` 读出来的是上次渲染的结果。如果不是自己触发刷新,很可能拿着几分钟前的快照下结论,然后和另一个刚刷新过页面的人得出完全相反的判断。
凡是回答「这项做完了没有」,先 `reload` 再读;两次读数不一致时以刷新后的为准,并把差异讲清楚。
小结
把这几条压成一句可执行的规则:**成功的定义必须是可观测的状态变化,而不是命令没有抛异常。**
具体落地就是三件事——操作前用 `elementFromPoint` 校验落点、操作后回读同一个指标做前后对比、读状态前先刷新页面。做到这三点,「假成功」基本就消失了。
- 点赞
- 收藏
- 关注作者
评论(0)