浏览器自动化的七个坑:从「点不动」到「点了没反应」
最近用浏览器自动化跑一批固定流程。流程本身不难——填表、点按钮、翻页。难的是每一步都可能「看起来成功了,其实没生效」。
这篇记录七个实际踩到的坑。它们不属于任何框架的文档,属于"你得自己撞一次才知道"的类型。
坑一:坐标点击对某些组件无效
常规的点击是往某个坐标派发一次 click。但很多组件库监听的不是 click,而是更靠前的指针事件。
一个 Vue 生态的下拉选择器,正确的事件序列是:
pointerdown → mousedown → pointerup → mouseup → click
只发 click,下拉会弹出、选项会高亮,但选中值不写回。表现是:你以为选好了,提交时报「请选择」。
这类组件的内部状态机通常是这样:pointerdown 时记录候选,pointerup 时判定「在同一元素上释放」才提交。跳过头一个事件,状态机进不去。
判断方法:如果点击后 UI 有反应(高亮/展开)但数据没变,几乎一定是事件序列不全。
坑二:提交按钮可能不是你看到的那个元素
页面上有个「报名」按钮,视觉上是一个 <div class="btn-block confirm">。点它,什么都没发生。
看 DOM 才发现:
<div class="btn-block confirm">
<a id="sub" class="btn btn-red" href="javascript:void(0);">报名</a>
</div>
真正的 handler 绑在里面的 <a> 上,外层的 div 只是个布局容器。坐标落在 div 的 padding 区域时,点击事件不会被内层元素收到。
教训:点之前先用 elementFromPoint(x, y) 确认那个坐标上到底是什么。
坑三:复选框不是 input
需要勾选一个协议。document.querySelectorAll('input[type=checkbox]') 找到 7 个,全是 width: 0——它们是被隐藏的真实 input,视觉上的勾选框是一个空的 <span>,点击靠 CSS + JS 自己实现。
<span id="agreementBox" mate_data_ts="....click.免责声明"></span>
对隐藏 input 调 .click() 会改 checked 属性,但框架不读它——它读的是自己的状态。要直接点那个 span。
配套现象:按钮一直是 disabled 的灰色状态,直到 span 的 class 变成 checked。
坑四:编辑器在 iframe 里,且只渲染视口
富文本编辑器(比如 TinyMCE)在一个跨域 iframe 里。直接 querySelector 拿不到内容,得走它自己的 API:
window.tinymce.activeEditor.setContent(html);
window.tinymce.activeEditor.fire("change"); // 少了这句,框架认为没改过
window.tinymce.activeEditor.save(); // 少了这句,提交的是空内容
fire("change") 和 save() 是最容易漏的两步:内容确实进去了,但表单状态还是「未修改」,提交后是空白。
更隐蔽的是另一类编辑器(CodeMirror 6):它只渲染视口内的行。所以
document.querySelector(".cm-content").innerText.length
拿到的是可见部分的长度,不是全文。我用它校验「正文是否完整粘贴」,得到的结论是「少了 60% 内容」——实际上内容完整,只是屏幕外没渲染。
正确做法:滚到顶部看开头、滚到底部看结尾,两端对上即可。或者直接读状态:cmView.view.state.doc.length。
坑五:窗口最小化时,所有坐标都是假的
浏览器窗口最小化后:
window.innerWidth → 0
window.innerHeight → 0
此时所有 getBoundingClientRect() 返回的都是相对一个 0×0 视口的坐标。点 (1376, 199) 会落到别的地方,或者落到 (0,0)。
而且不会报错。脚本一路"成功"执行完,截图时才抛一句「无法在窗口最小化时截图」。
教训:操作前先断言 window.innerWidth > 0。这是个一行成本的前置检查,能省掉一整轮排查。
坑六:响应式布局会让选择器集体失效
窗口宽度为 0 时,响应式样式会把卡片列表换成另一套 DOM 结构。同一个页面,宽度 1524 时有 10 个 .listBox,宽度 0 时有 0 个。
所以「选择器找不到元素」有时不是元素不存在,是布局分支不同。加上窗口最小化这个前提,就会得到一个很误导的结论:脚本昨天还能跑,今天所有 querySelectorAll 返回空。
坑七:hover 是前置依赖
有些按钮在 hover 时才挂载真正的点击逻辑(比如「Star」按钮:鼠标移入后展开成一个带图标的状态按钮)。直接 click 会命中还没初始化完的元素。
正确顺序:
await mouse.move(x, y); // 先 hover
await sleep(400); // 等组件展开
await mouse.click(x, y); // 再点
一个通用的调试顺序
踩完这七个坑,我现在的排查顺序固定成三步:
- 先验证环境:窗口尺寸、页面是否是最新的、登录态是否有效
- 再验证元素:
elementFromPoint确认坐标上是什么;getBoundingClientRect的宽高是否非零 - 最后验证状态:不要相信「点过了」,去读结果——按钮 class 变了吗?列表里多了一条吗?接口返回了什么?
第三条最重要。前面六个坑的共同点是:动作成功,状态未变。自动化脚本最容易犯的错,就是把「我执行了动作」当成「事情发生了」。
检查状态永远比检查动作可靠。这也是为什么我后来在每个关键步骤后面都加一行断言——读一次页面状态,比回看十行日志有用。
- 点赞
- 收藏
- 关注作者
评论(0)