登录表单里有两个密码框:Input.insertText 写进账号框的排查
登录表单里有两个密码框:Input.insertText 写进账号框的排查
现象
用 CDP 驱动一个账号密码登录页,脚本输出 typed 12 chars,看起来一切正常。提交后提示账号或密码错误。回读表单:
{"u":"18826418077smx455486589","plen":0}
账号框里是「账号 + 密码」的拼接串,密码框 value.length 为 0。CDP 没有报错,Input.insertText 返回成功,页面也没有异常。
第一层原因:insertText 的目标是「当前焦点元素」
Input.insertText 不接收选择器,它把文本插进文档当前焦点所在的元素。所以焦点错了,文本就会静默落到上一个有焦点的输入框里,而且命令照常返回成功。
排查的第一步是打印焦点,而不是猜测插入位置:
document.activeElement.tagName + '/' + document.activeElement.type
本次输出是 INPUT/text,目标应该是 INPUT/password。焦点没生效。
第二层原因:页面里有两个密码框,取到的是隐藏副本
该登录页同时渲染了「手机号登录」和「密码登录」两套表单,DOM 里对应两组输入框。把全部 input 摊开看:
[...document.querySelectorAll('input')].map((e, i) => ({
i, name: e.name, type: e.type,
w: Math.round(e.getBoundingClientRect().width),
len: e.value.length,
}))
结果里 input[type=password] 出现四次,其中只有一个是可见的:
| 序号 | name | type | width | 说明 |
|---|---|---|---|---|
| 0 | — | password | 0 | 隐藏副本 |
| 1 | — | password | 398 | 当前生效的密码框 |
| 2 | — | password | 0 | IAM 表单残留 |
| 3 | — | password | 0 | IAM 表单残留 |
document.querySelector('input[type=password]') 按文档顺序返回 0 号那个 width=0 的元素。对不可见元素调用 focus() 不报错,但焦点根本不会转移,activeElement 仍是上一次点击过的账号框。后续 insertText 就顺理成章地追加到了账号后面。
所以真正的判据不是「focus() 有没有抛异常」,而是两件事同时成立:
p.getBoundingClientRect().width > 0
document.activeElement === p
第三层原因:切 tab 之后 autofill 不会再触发
最初的方案是「用浏览器已保存的密码,等它自动填充」。实际观察到:
document.querySelectorAll('input:-webkit-autofill').length // 0
原因是自动填充在页面加载时只对可见的密码表单生效。登录页默认停在扫码/手机号 tab,密码表单当时是隐藏的,浏览器跳过;之后用 JS 点击切到「密码登录」,浏览器不会重新尝试填充,表单就一直是空的。这一步没有报错,也没有任何 API 能感知「没有填充」,只能靠回读 value.length === 0 判断。
修复
三处改动,缺一处都会回到原状态:
- 所有元素查询先按可见性过滤,不要把
querySelector的第一个命中当目标; - 写入值用原生 setter + 派发事件,绕开框架对
value的劫持(Angular/React 受控组件直接赋值不会同步到模型); - 写完立刻回读长度,把「命令返回成功」和「值真的写进去了」分开验证。
const vis = s => [...document.querySelectorAll(s)]
.filter(e => e.getBoundingClientRect().width > 0);
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value').set;
const p = vis('input[type=password]')[0];
p.focus(); // 先拿焦点
if (document.activeElement !== p) throw new Error('focus failed');
setter.call(p, pwd);
p.dispatchEvent(new Event('input', { bubbles: true }));
p.dispatchEvent(new Event('change', { bubbles: true }));
顺序也要注意:先写密码、再写账号。如果先写账号,写密码失败时页面上不会出现任何异常信号,只有提交那一刻才会暴露。
修复后回读 {"u":11,"p":12},两条长度都对得上;登录成功后 URL 带回 ?ticket=ST-...,这才是最终判据——不要用「页面上没有登录按钮了」当成功标志,登录中跳转的中间页本来就没有登录按钮。
可复用的结论
Input.insertText的成功返回只代表「事件派发成功」,不代表文本进了预期元素。任何自动化输入之后都必须回读目标元素的value.length或innerText.length。- 同名元素取到隐藏副本是通用坑,尤其是响应式布局和「多套表单并存」的登录页。统一用
getBoundingClientRect().width > 0过滤,再用elementFromPoint或activeElement回验。 - UI 状态切换(切 tab、开关弹窗)之后要重新取元素,之前缓存的引用可能已经指向被隐藏的那一份。
- 框架页面的写入要走原生 setter,直接赋值或依赖插入文本都可能绕过或丢失模型同步。
这类问题的共同特征是「全程无报错」,所以排查不能靠看日志,只能靠在关键步骤插入断言:焦点是谁、元素可见吗、值长度是多少。三个断言加上去,定位成本从一小时降到一分钟。
- 点赞
- 收藏
- 关注作者
评论(0)