表单填到一半就没了:自动化里三类"页面自己变了"的坑

举报
yd_251777447 发表于 2026/10/30 14:05:20 2026/10/30
【摘要】 写自动化脚本最挫败的时刻,不是报错,是 它跑完了、日志全绿、结果却没有 。 页面没崩,选择器没失效,每一步都返回成功。但提交上去的东西是空的。 这类问题有个共同点:页面在你不知道的时候自己变了一次。下面三个都是实测踩过的,来自给一个带 SSO 的站点做登录后操作。每个坑后面附了怎么提前发现。 一、SSO 重定向把已填内容清空 场景:页面打开了,标题填了、正文填了、标签加了,点发

写自动化脚本最挫败的时刻,不是报错,是它跑完了、日志全绿、结果却没有。

页面没崩,选择器没失效,每一步都返回成功。但提交上去的东西是空的。

这类问题有个共同点:页面在你不知道的时候自己变了一次。下面三个都是实测踩过的,来自给一个带 SSO 的站点做登录后操作。每个坑后面附了怎么提前发现。

一、SSO 重定向把已填内容清空

场景:页面打开了,标题填了、正文填了、标签加了,点发布,弹窗提示"请填写标题"。

打开截图一看,编辑器是空的。

原因是 URL 里多了个东西:

?ticket=ST-1234-xxxx-cas

带票据的这次跳转是一次完整的页面加载。浏览器不会保留上一个文档里 DOM 表单的值——那些内容从来没进过后端,只活在内存里。页面一刷新,全没了。

麻烦在于这个跳转的时间点不固定。可能在你打开页面 3 秒后,也可能在你点了发布之后。看着像"发布失败",其实是"提交的根本不是你填的那份"。

怎么提前发现

在提交前,比对填完之后拿到的值,和 DOM 里现在的值:

const before = await page.locator('#title').inputValue();
// ...其它操作
const after = await page.locator('#title').inputValue();
if (before !== after) throw new Error('页面被重置,内容丢失');

更彻底的监测是监听导航事件,任何一次预期外的跳转都记下来:

let navs = [];
page.on('framenavigated', f => {
  if (f === page.mainFrame()) navs.push({ url: page.url(), t: Date.now() });
});

跑完看 navs,如果中间多了一次带 ticket= 或 sso 的加载,基本就锁定了。

怎么办

两条路。一是等它跳完再填——但跳转时机不可控,这招不稳。二是绕开 DOM,直接调页面暴露的提交接口。带 SSO 的系统通常都有一层前端封装函数,翻一下页面源码往往能找到:

// 页面里已存在的原生函数,直接复用它的 CSRF 处理和字段组装
await page.evaluate((payload) => window.submitPost(payload), data);

这条路的前提是能读懂它的字段契约,后面第四节说。

二、编辑器不在你以为的那个文档里

第二个坑更隐蔽:选择器完全正确,locator.fill() 也不报错,但内容就是没进去。

因为编辑器在 iframe 里。

而且很多富文本编辑器用的是 CodeMirror 6,正文不在 textarea、不在 contenteditable 的普通 div 里,而在:

<div class="cm-content" contenteditable="true"></div>

它的内容由编辑器内部状态维护,直接设 innerText 会被下一次重渲染抹掉。

正确写法是先定位 frame,再聚焦,再用键盘输入:

const frame = page.frameLocator('iframe#editor-frame');
const body = frame.locator('.cm-content');

await body.click();                       // 必须先获得焦点
await page.keyboard.insertText('正文…');   // insertText 比 type 快且不改触发顺序

insertText 会把整段文本作为一次输入事件写入,不像 type 逐字符模拟。对 CodeMirror 这类自己接管输入事件的组件,前者可靠得多。

判断是不是这类问题

一句话:fill() 成功了但页面预览没变化,就去看 iframe。

console.log(page.frames().map(f => f.url()));

如果正文所在的 frame 有一堆自己的子资源,说明渲染是独立的,主文档的选择器根本够不着它。

同一类坑还有"自定义勾选框":看着是个 checkbox,实际上是个 ,没有 type 属性,check() 直接报找不到元素。这种只能点它本身,然后靠下游按钮的状态变化来验证是不是真勾上了——比如发表按钮的 class 从 xxx-no 变成 xxx。

别信"我点了",只信"状态变了"。

三、MFA 静默拦截:看起来只是没登录

第三个坑最费时间,因为它不给任何错误。

流程是扫码登录,扫完,页面回到目标站点,一切正常。但页面显示的仍然是未登录状态:积分是 -,按钮是"去完成"。

第一反应是"扫码没成功"。其实成功了,卡在下一步:

https://auth.example.com/authui/login.html#/loginBindMfa

账号要求双因素验证,页面提示"账号未开启双验证",等你选"暂不开启"。选完才放行。

问题在于这个页面不报错、不弹窗、不跳转。它只是安静地停在那儿。而你的脚本已经往下走了,看到的是"目标页面加载成功",于是判定登录失败。

怎么判断状态

别用 URL 判断,URL 会骗人。用业务页面上的特征文本:

const state = await page.evaluate(() => {
  const t = document.body.innerText || '';
  return {
    needLogin: /登录\/注册查看/.test(t),
    user: (t.match(/\b[a-z]{2}_\d{6,}\b/) || [null])[0],
  };
});
if (state.needLogin || !state.user) { /* 停下来,交还控制权 */ }

有用户名才算登录。needLogin 这类文案是 UI 的兜底显示,比任何接口返回都诚实。

拦截链要按顺序走完

登录流程往往是多级的:扫码 → 可能是 MFA → 可能是协议签署 → 才到目标页。每一步都会改变"登录态"的判定结果,所以要逐级处理,每级都截图落盘:

await page.goto('https://console.example.com/');
if (/loginBindMfa/.test(page.url())) {
  await page.locator('input[type="checkbox"]').first().check();
  await page.getByText('暂不开启', { exact: true }).click();
  await page.waitForTimeout(8000);
}

waitForTimeout 在这里不是偷懒。协议页和 MFA 页的放行是异步的,没有可靠的 DOM 信号可以等,固定等待加一次校验是最实际的做法。

四、换个层次解决问题:读接口契约

上面三个坑,本质上都是"在 DOM 层做本该在请求层做的事"。

DOM 是给人用的,会重渲染、会被 iframe 隔离、会被中间页打断。而请求层是稳定的。

当页面里已经加载了一个完整的提交函数,最稳的做法不是模拟点击,而是搞清楚它的字段契约,直接调用:

// 1. 找到页面已经解析好的那个函数
const src = await page.evaluate(() => window.submitPost.toString());
// 2. 读它组装的字段:哪些必填、哪些是常量、CSRF 从哪来
// 3. 自己组一份等价的数据传进去
await page.evaluate((d) => window.submitPost(d), payload);

好处是:没有重定向清空问题(不起 DOM)、没有 iframe 问题(不碰编辑器)、错误码是结构化的(不是"发布失败"四个字,而是具体 code)。

代价是要花时间读源码,而且接口变了脚本会静默失效——所以要配合第一节的校验:提交后立刻拿到返回的 ID,再用这个 ID 去查一次。

const res = await page.evaluate((d) => window.submitPost(d), payload);
if (!res || res.code !== 'OK') throw new Error(`提交失败: ${JSON.stringify(res)}`);
const id = res.data.id;

有了 ID,后面的核对就有依据了。没 ID 的成功,都是猜测。

五、小结

三个坑,一个共同规律:

  • SSO 重定向会清空 DOM 里的内容 → 提交前比对值,或监听 framenavigated
  • iframe + CodeMirror 让选择器"正确但无效" → 用 frameLocator,聚焦后 insertText
  • MFA 静默拦截不报错只是不通行 → 用业务特征判断登录态,不要信 URL
  • 页面会自我重渲染,只信状态变化,不信"我点了"
  • 能读接口契约就别硬模拟 DOM,但接口调用必须配 ID 级校验

判断标准可以简化成一句:你的脚本有没有一个"提交之后能独立验证"的锚点。有,坑就只是麻烦;没有,坑就会变成"跑通了但不对",而后者更难查。


文中代码基于 Playwright + Windows 实测,站点已做脱敏处理。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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