登录表单里有两个密码框:Input.insertText 写进账号框的排查

举报
yd_228692151 发表于 2026/11/04 06:44:54 2026/11/04
【摘要】 登录表单里有两个密码框:Input.insertText 写进账号框的排查 现象用 CDP 驱动一个账号密码登录页,脚本输出 typed 12 chars,看起来一切正常。提交后提示账号或密码错误。回读表单:{"u":"18826418077smx455486589","plen":0}账号框里是「账号 + 密码」的拼接串,密码框 value.length 为 0。CDP 没有报错,Inp...

登录表单里有两个密码框: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 判断。

修复

三处改动,缺一处都会回到原状态:

  1. 所有元素查询先按可见性过滤,不要把 querySelector 的第一个命中当目标;
  2. 写入值用原生 setter + 派发事件,绕开框架对 value 的劫持(Angular/React 受控组件直接赋值不会同步到模型);
  3. 写完立刻回读长度,把「命令返回成功」和「值真的写进去了」分开验证。
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,直接赋值或依赖插入文本都可能绕过或丢失模型同步。

这类问题的共同特征是「全程无报错」,所以排查不能靠看日志,只能靠在关键步骤插入断言:焦点是谁、元素可见吗、值长度是多少。三个断言加上去,定位成本从一小时降到一分钟。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。