用 CDP 做浏览器自动化:三个必须知道的状态检查
## 引子
浏览器自动化最让人抓狂的不是报错,而是「看起来成功了但没生效」。
日志显示点击成功,页面毫无反应。没有异常,没有提示,只有结果不对。
这段时间踩了不少坑,总结出三个状态检查,能定位绝大多数失败。
## 检查一:页面有焦点吗
```javascript
document.hasFocus() // false = 页面失焦
document.activeElement // BODY = 没有元素获得焦点
```
**为什么重要**
Chrome 在多标签环境下,非前台标签的合成输入事件会被丢弃或降权。即使你用 CDP 直接发 Input.dispatchMouseEvent,页面失焦时也可能不处理。
**解法**
```python
ws.call("Page.enable")
ws.call("Page.bringToFront")
ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True}) # 关键
```
加上这三行之后,之前完全点不动的按钮立刻生效。
这个命令在 CDP 文档里不太显眼,但它解决的问题很实际。
## 检查二:坐标命中的是谁
```javascript
document.elementsFromPoint(x, y)
// ["SPAN.status-btn__badge-wrap", "A.commonBtn_green", "DIV.examChartBox1", ...]
```
**为什么重要**
按钮中心点可能命中的是内部的装饰元素(徽标、图标、文字),而不是按钮本身。如果内部元素有独立的 click handler 或者 pointer-events 设置,点击就落空了。
我遇到过的实例:一个 Star 按钮,中心点是数字徽标,点击后徽标动画播了,但 Star 状态没变。改成命中按钮的 left+3 位置才成功。
**解法**
不要在中心点硬碰,先 dump 元素栈:
```javascript
const stack = document.elementsFromPoint(x, y).slice(0, 5)
.map(e => e.tagName + '.' + String(e.className).slice(0, 30));
console.log(stack);
```
如果最顶层不是目标元素,就调整坐标,或者直接给目标元素派发事件。
## 检查三:事件可信吗
```javascript
el.dispatchEvent(new MouseEvent('click', {bubbles: true}))
// event.isTrusted === false
```
**为什么重要**
有些页面做了 isTrusted 检查,只响应真实用户事件。合成事件会被静默忽略。
**解法**
两级方案:
```python
# 一级:用 CDP 的 Input 域(生成的 isTrusted = true)
ws.call("Input.dispatchMouseEvent", {"type": "mousePressed", ...})
# 二级:CDP 也失效时,直接调框架方法
```
二级方案是找到 Vue/React 的组件实例,直接调用它的方法,绕过 UI 层。缺点是跳过了界面校验,可能触发不一致状态,作为最后手段。
## 三个检查的顺序
| 顺序 | 检查 | 耗时 | 命中率 |
|---|---|---|---|
| 1 | document.hasFocus() | 1 秒 | 约 60% |
| 2 | elementsFromPoint | 2 秒 | 约 25% |
| 3 | isTrusted / 调框架方法 | 30 分 | 约 10% |
前两个是秒级检查,先做。第三个代价大,最后考虑。
## 完整可用模板
```python
def ensure_ready(ws):
ws.call("Page.enable")
ws.call("Page.bringToFront")
try:
ws.call("Emulation.setFocusEmulationEnabled", {"enabled": True})
except Exception:
pass
def click(ws, 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)
```
**关键在回读**:每次点击后都读一次状态,断言符合预期。不复核的自动化等于没有自动化。
## 文字输入的坑
Input.insertText 对某些 contenteditable 不生效:
```python
# 无效:输入框内容长度 0
ws.call("Input.insertText", {"text": "你好"})
# 有效:逐字符派发 char 事件
for ch in "你好":
ws.call("Input.dispatchKeyEvent", {"type": "char", "text": ch})
time.sleep(0.08)
```
同样的问题:不报错,只是没输入。
## 小结
自动化失败分两类:显式失败会抛异常,好排查;静默失败无声无息,最难查。
三个检查针对的都是静默失败:
1. **焦点**:页面不聚焦,输入被丢弃
2. **遮挡**:坐标命中装饰元素,点了个寂寞
3. **可信度**:页面拒绝合成事件,静默忽略
每个都能用一行代码诊断。遇到「点不动」,先跑这三行,不要急着换坐标。
- 点赞
- 收藏
- 关注作者
评论(0)