自动化工具 E2E 测试里「点了但没选上」的假成功,怎么用组件插件堵住

w471176877 · 2026年10月14日 · 144 次阅读

一、从一次「漂亮的三连绿」说起

1. 点击  下拉框          → PASSED
2. 点击  选项「上海」      → PASSED
3. 断言  表单值为「上海」   → FAILED

前两步日志干干净净,第三步告诉你没选上。原因不神秘:Ant Design / Element / Vant 的下拉不是原生 <select>,你点开的「选项」是个 <div>,点它只是让 DOM 变了个样,表单状态并没有提交。

这类问题在社区里讨论过很多次,本质是一句话:动作成功 ≠ 业务终态达成。这篇文章聊我们用什么办法把它堵住 —— 一个跟着被测页面跑的插件系统,以及里面几条被踩出来的约定。

插件 Tab:来源、所属预设、动作三列;工具栏是上传插件 / 插件模板 / 插件技能

二、插件做的三件事,缺一不可

先说边界:插件不管「怎么找到元素」(那是定位器的事),它管三件具体的事:

  1. 归属判定:这个元素属于哪个组件,谁来处理?
  2. 语义动作:对「选一个选项」这种意图,正确的操作序列是什么?
  3. 后验确认:动作做完了,怎么在页内确认「真的选上了」?

只做第 2 步,就是把裸 click 换成封装好的 click,假成功还在。真正拦下假成功的是第 3 步。

现在的插件覆盖四个组件库变体(Ant Design、Element 的 element-ui/element-plus、Vant、MUI),按能力域拆开:下拉选择、树形选择、日期选择、时间选择、滑块、级联选择、勾选、单选。原生 <select> 不走插件,由原生交互层兜底 selectOption。

三、形态:一份纯页内脚本,不做平台依赖

插件就是一个求值为「插件定义对象」的表达式:

({
  detect:      (el) => ...,          // 1. 归属判定
  candidates:  (el) => [...],        // 2. 候选定位器
  annotate:    (el) => '...',        // 3. 一句话语义标注
  actions:     { select: { ... } },  // 4. 语义动作
})

几个刻意的设计,都是为了少踩坑:

  • 只在被测网站的浏览器环境执行,平台 Node 侧不含插件代码;
  • 不需要 IIFE 外壳、不需要调用 register、不需要自带 id,平台注入时自动封装登记(要内部辅助函数就写成 (() => {...; return {...}; })());
  • 不需要构建降级 —— 它只在 Playwright 的 Chromium 里跑,const / 箭头函数 / 可选链直接用;
  • 单文件 ≤ 512KB,单 .js 或 zip(manifest.json 只写 {"entry": "index.js"})。

四、几个容易写错的地方

detect 要宽进,但注册了动作的插件必须精确。 每个元素都会被问「你归谁管」,命中谁,动作就派给谁。如果类名写得太泛,动作会被误派给不该处理的组件。

弹层内容必须锚定容器。 弹层常常 portal 到 body 下,而且是常驻实例(隐藏但不销毁)—— 不锚定作用域,就会命中那个隐藏实例,于是「明明可见的选项」定位失败。

后验不能只看回显。 这条最容易写错:可搜索下拉在筛选时会把输入文本回显到输入框里,所以「输入框里显示了上海」不等于上海被选中了。必须校验组件自己的选中状态标记,否则「筛选了但没点中」会被判成成功。

状态相关的多步操作必须在一个动作里闭环(打开 → 定位 → 选中 → 等回显),不允许返回「还需要点 N 次」让外壳拆步 —— 外壳拆出来的步骤既不稳定,语义也丢了。

动作要幂等。 已经是目标终态就直接返回成功:匹配链上排在前面插件可能已经做了一半。

五、三态结果协议:不做含糊处理

每个动作只有三种结果:

返回 含义
返回字符串 success(字符串作为结果消息)
throw Error failed(错误信息给用户/模型看)
返回 {status, message} 显式三态:success / failed / uncertain

uncertain 是特意留的口子:动作执行完了,但此刻读不到足够证据判断终态。这种交给平台的效果哈希兜底去核对,而不是让插件拍脑袋报「成功」。后验 verify 不过,视同 failed —— 这就是拦下「点了但没选上」的那个开关。

我们的观点是:宁可返回一个待定,也不要返回一个假的成功。 前者最多多花你几秒钟看证据,后者会让你在两周后才发现某个断言其实早就失效了。

六、跨进程的 pw 桥,以及它的代价

页内脚本能力有限:拿不到真实键盘事件的时序、没有 auto-wait、等网络要自己写。所以动作的第三个参数是一个 Playwright 桥(在 Node 侧执行):

async fn(el, args, pw) {
  const root = await pw(el);                       // 页内元素 → Node 句柄
  await root.locator('input').fill('上海');         // auto-wait 真实 fill
  await pw.page.keyboard.press('Escape');           // 真实键盘事件
  await pw.page.waitForLoadState('networkidle');    // 等网络空闲
}

代价得说清楚:每次 pw 调用都是一次跨进程往返,适合用在关键几步,不适合放进高频轮询;等待类需求交给 Playwright 的 auto-wait。另外,脱离平台外壳运行时(比如在页面控制台里手搓)会直接报「桥未注入」—— 这是故意的,免得你以为在用真实 Playwright,其实调了个空壳。

七、注入与匹配链:组合即开关

插件不是上传就生效,这是有意的设计:

项目 → 关联的预设 → 预设成员(按优先级) → 注入浏览器
  • 项目关联了预设,里面的插件才会被注入;没关联的插件在列表里会标注「暂不会被注入」;
  • 成员按优先级排成匹配链:顺序数字越大优先级越高、越先注入也越先尝试,先成功即止;新加入的成员自动拿最高优先级;
  • 未关联预设时回落到内置的「默认组合」,所以新建项目立刻有组件能力。

注入顺序还有一条不能动的规则:运行时框架脚本先行,成员插件随后。插件内部有安装守卫,顺序颠倒会静默跳过注册 —— 症状是「插件装了但动作不见了」,很难查。

覆盖内置是允许的:把自己的插件排在内置插件之前就能增强或覆盖。但有一种情况要小心:如果你的组件与某个已注册动作的内置插件共享根类(比如 antd 的 TimePicker 与 DatePicker 同为 .ant-picker),就得同步收窄既有插件的归属判定并加误派保护,否则动作会派错。

预设 Tab:成员编排(拖拽调优先级)与项目关联

八、验收:先跑 harness,再谈上传

插件模板下载下来是个能跑的小项目:

npm install
npx playwright install chromium
npm run harness -- sample-widget index.js --probe

会弹浏览器扫示例页面,期望看到「候选 ✓唯一」标记和「✅ 验收通过」以及动作清单。验收只看两件事:候选唯一、动作清单齐全(harness 不执行动作);要自测动作就加 --hold,在控制台里直接调:

await window.__ttPluginRegistry__.invokeAction('demo-select', 'select', document.querySelector('.demo-select'), { value: '上海' })
// 期望 { status: 'success', message: '已选择:上海' },且页面下拉框已回显

上传之后还要点一次「试运行」:插件的动作清单要在页内注册后才拿得到,试运行会把「命中数、候选总数、动作列表」回写回去。它顺带解决了一个体验问题 —— 任意可打开的页面都能试运行,不需要页面里真有目标组件。更新插件也不用删了重传,操作列的「重新上传」替换源码即可。

步骤表:动作名与定位策略都会落进脚本

用例详情:动作 / 选择策略 / 定位值三列都能编辑

九、几个明确的「不做」

  • 不把插件做成「什么都能处理」:注入的成员越多、链越长,启动开销越大。覆盖要克制 ——「能处理这个组件」比「尽量多命中」重要。
  • 不让插件替你做业务断言:插件只保证「组件的终态达成了」,业务对不对仍然靠断言。这两件事混一起,插件就变成「什么都替你判成功」的黑盒了。
  • 不静默吞掉错误:三态协议里没有「大概成功」。

有一个前提必须提醒:插件代码是在被测网站的浏览器环境里执行的,它能读页面、能操作页面。所以「只启用你理解过的插件」不是客套话 —— 界面上也写了这句提示。

运行日志:每一步的结果都在这里,插件的后验失败会落成 failed

十、想听听你们的做法

组件适配这件事,各家走的路不太一样。我见过:给组件库写专门的 Page Object、要求开发加 data-testid 后只用 testid 定位、用视觉/坐标点击绕开 DOM、以及在 CI 里跑「组件冒烟」逐个验证下拉/日期能用。

你们团队怎么处理这类「点不上」的组件?有没有遇到过那种『日志全绿、业务没生效』的假成功? 欢迎在评论区聊聊。

现状与限制

利益相关:我是 TestDog 的作者,本文写的是自己的实现与踩坑,不构成第三方评测。

暫無回覆。
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册。