浏览器自动化里最顽固的坑:合成鼠标点击失效
## 现象
用 CDP 做浏览器自动化,遇到一个按钮怎么都点不动:
```
元素定位正确 document.querySelectorAll('a.jobExamComBtn')[0] -> 有
坐标正确 getBoundingClientRect() -> (1265, 226, 82x28)
事件派发了 Input.dispatchMouseEvent mousePressed / mouseReleased
↓
但页面毫无反应,日志显示"已点击"
```
试了一下午,逐个排查。
## 尝试一:换个点击坐标
怀疑坐标算错,把按钮矩形的每个位置都试一遍——中心、左上角、left+3、top+height/2:
```javascript
const rr = btn.getBoundingClientRect();
// 全都试过,全部无效
[{x: rr.left + rr.width/2, y: rr.top + rr.height/2},
{x: rr.left + 3, y: rr.top + rr.height/2},
{x: rr.left + 5, y: rr.top + rr.height/2}]
```
**结论:不是坐标问题。**
用 `elementsFromPoint` 确认了该坐标下最顶层的元素:
```javascript
document.elementsFromPoint(1306, 468)
// ["SPAN.themeBg", "A.commonBtn_green jobExamComBtn", ...]
```
命中的确实是按钮本身。
## 尝试二:绕过坐标,直接派发事件
如果坐标点击有问题,那把完整的事件序列直接派发到元素上:
```javascript
const o = {bubbles: true, cancelable: true, view: window,
clientX: cx, clientY: cy, button: 0};
el.dispatchEvent(new PointerEvent('pointerdown', o));
el.dispatchEvent(new MouseEvent('mousedown', o));
el.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, o, {buttons: 0})));
el.dispatchEvent(new MouseEvent('mouseup', Object.assign({}, o, {buttons: 0})));
el.dispatchEvent(new MouseEvent('click', Object.assign({}, o, {buttons: 0})));
```
**结论:仍然无效。**
顺便检查了 `isTrusted`:
```javascript
// dispatchEvent 派发的事件 isTrusted 永远是 false
// 如果页面做了 isTrusted 检查,这条路必然失败
```
## 尝试三:调 Vue 组件的方法
页面是 Vue 2,试图直接调用组件方法:
```javascript
// 找 Vue 实例
let root = null;
document.querySelectorAll('*').forEach(e => {
if (e.__vue__ && !root) {
let v = e.__vue__;
while (v.$parent) v = v.$parent;
root = v;
}
});
// 递归找目标组件
function walk(vm, depth) {
if (depth > 10) return;
if (vm.$options.name === 'job-list-vue') { console.log(vm.$options.methods); }
(vm.$children || []).forEach(c => walk(c, depth + 1));
}
```
找到了 `goDohomework`、`applyRework` 等方法,但**数据是空的**:
```
workLists: [] <- 关键数据没加载
```
调用方法等于空转。
## 真正的原因:页面失去焦点
最后试了一个 CDP 的冷门命令:
```python
ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True})
```
**加上这一行,点击立刻生效。**
### 为什么会这样
Chrome 在多标签页环境下,非前台标签的合成鼠标事件会被丢弃或降权。`Emulation.setFocusEmulationEnabled` 让渲染器认为页面处于聚焦状态,从而正常处理合成输入。
验证方法:
```javascript
document.hasFocus() // false = 页面失焦
document.activeElement // BODY = 没有元素获得焦点
```
如果这两个都不对,先开焦点模拟。
## 完整可用的点击函数
```python
def click(x, y, wait=1.5):
ws.call("Input.dispatchMouseEvent", {
"type": "mouseMoved", "x": x, "y": y})
time.sleep(0.25)
ws.call("Input.dispatchMouseEvent", {
"type": "mousePressed", "x": x, "y": y,
"button": "left", "buttons": 1, "clickCount": 1})
time.sleep(0.12)
ws.call("Input.dispatchMouseEvent", {
"type": "mouseReleased", "x": x, "y": y,
"button": "left", "buttons": 0, "clickCount": 1})
time.sleep(wait)
```
前提是先执行:
```python
ws.call("Page.enable")
ws.call("Page.bringToFront")
ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True}) # 关键
```
## 另外两个坑
### 坑一:文字输入要用 dispatchKeyEvent
```python
# 无效:内容长度 0
ws.call("Input.insertText", {"text": "你好"})
# 有效:逐字符派发
for ch in "你好":
ws.call("Input.dispatchKeyEvent", {"type": "char", "text": ch})
time.sleep(0.08)
```
`Input.insertText` 对某些 contenteditable 容器不生效,逐字符 `char` 事件更可靠。
### 坑二:侧边导航不保存数据
答题系统侧边有题号导航,点它能跳转——但**答案不会被保存**:
```
点侧边题号 1→2→3→...→10 → 全部作答,完成率 0%
点「下一题」 1→2→3→...→10 → 全部作答,完成率 100%
```
这是两套不同的逻辑:导航只做显示切换,`下一题` 才触发保存。第一次提交拿了 0 分,重做才回到 16 分。
## 排查清单
自动化点不动时,按顺序查:
| 顺序 | 检查项 | 命令 |
|---|---|---|
| 1 | 页面是否有焦点 | `document.hasFocus()` |
| 2 | 元素是否被遮挡 | `document.elementsFromPoint(x,y)` |
| 3 | 是否检查 isTrusted | 看页面源码是否有该判断 |
| 4 | 是否需要焦点模拟 | `Emulation.setFocusEmulationEnabled` |
| 5 | 是否有 iframe | `Page.getFrameTree()` |
| 6 | 是否 Shadow DOM | `el.shadowRoot` |
前三项都对还点不动,第四项基本就是答案。
## 小结
```
点击失败的三种层次:
坐标错 -> 换坐标,好排查
事件假 -> isTrusted,绕不过
没焦点 -> 一行命令解决,最难想到
```
最坑的地方在于:**它不报错**。日志显示"已点击",元素也确实拿到了,只有结果不对。这类"假成功"比报错难查十倍。
遇到这种情况,与其反复调参,不如先把状态读出来——`document.hasFocus()` 一行就能定位。
- 点赞
- 收藏
- 关注作者
评论(0)