自动化工具 为什么你录的定位器活不过一次改版?聊聊定位器拾取里的几个细节

w471176877 · 2026年10月09日 · 139 次阅读

一、那个所有人都遇到过的现象

录脚本的时候一切正常:点登录按钮、选下拉、断言弹窗出现,全部通过。第二天跑回归,红的不是一条两条 —— 而且失败信息都长得很像「找不到元素」。

打开页面看一眼,元素明明还在,只是:

  • 按钮从 <div class="btn-primary"> 换成了 <button>;
  • 列表外层多包了一层 <div>,nth-child(3) 从此指向别的行;
  • 弹窗里的元素,昨天在 body 下面第 7 个位置,今天多挂了一个 portal,变成第 9 个。

定位器能活多久,基本在你写下它的那一刻就定了。这篇文章聊我们在这方面做的几个选择,以及踩到的三个坑 —— 这三个坑的共同点是:录的时候完全正常,回放的时候必然失败。

步骤表:策略、定位值与作用域都可以逐条编辑

二、先排序:testid > role > label > placeholder > text > alt > title > css > xpath

这是全项目的统一口径,手动拾取、生成、失败自愈三处用的是同一份候选逻辑(口径不一致是更难查的问题)。

排前面的理由很朴素:

  • testid(data-testid)是给测试用的契约,不随样式和文案变;
  • role + 可访问名:走 ARIA 语义,既抗改版,也顺带保证元素对辅助技术可用;
  • label / placeholder / text:可读性最好,但依赖文案,改文案就失效;
  • css:结构唯一就能命中,但结构一变就废;
  • xpath:最后的兜底,只在主文档、且元素不在弹层里时生成。

有一条是刻意不做的:不为了凑出一个定位器而硬塞 nth-of-type。如果这个元素确实只有结构性路径可走,我们宁可这条候选不生成 —— 一个三个月后必然红掉的定位器,比没有定位器更浪费时间。

一份 4 步脚本:策略与值直接落在步骤表里

三、三个「录的时候好好的、回放必挂」的坑

坑 1:瞬态状态类被编进了 CSS

组件库的类名里混着大量随交互实时变化的状态类:open / focused / active / expanded / checked / selected / loading、status-success|error|warning|validating、Element Plus 的 is-focus / is-active / ...。

你拾取弹窗里的元素时,弹窗是展开状态,容器带着 open;回放时页面是初始态,弹窗还没展开 —— 定位器要么命不中,要么命中另一个「碰巧同类」的元素。

处理很简单:生成 CSS 之前把这批类剔掉。

// 编入 CSS 会导致回放(页面初始态)必不命中或命中错误元素
const TRANSIENT_CLASS_RE =
  /(^|-)(focused|open|active|expanded|checked|selected|loading)$|(^|-)status-(success|error|warning|validating)$|^is-(focus|focused|active|open|expanded|checked|selected|error)/;
const stableClasses = (el) => [...el.classList].filter((c) => !TRANSIENT_CLASS_RE.test(c));

这个正则现在有两份:页内候选生成一份、服务端验证一份(因为验证要在 Node 侧再算一遍唯一性)。两份必须同步,改一个忘另一个,就会变成「生成时剔了、验证时没剔」的错位。

坑 2:textContent 里的无障碍播报文本让文本值翻倍

Ant Design 的 Select 交互后会往 DOM 里插一个播报节点:

<span aria-live="polite" style="width:0;height:0;position:absolute;overflow:hidden;opacity:0">已选中的项</span>

textContent 会把这段播报文本和可见的选中项拼起来,「智能体平台」于是变成「智能体平台智能体平台」。

更要命的是它是交互期瞬态出现的:录的时候存在,回放的时候(静态 DOM)不存在,于是按它生成的 text 候选 100% 命中失败。

现在的做法是文本候选取可见文本(遍历时跳过 display:none / visibility:hidden / opacity:0 / sr-only 零尺寸节点),并且超过 80 字符就不做 text 候选。至于唯一性计数,仍然按 textContent 走 —— 这样和 Playwright getByText 会匹配隐藏文本的语义一致,被污染的候选会因为「匹配数 > 1」被自然拒掉。

坑 3:非标准 role 让 Playwright 直接抛错

getByRole 只接受标准 ARIA role。元素上如果写着 role="xxx-custom",生成的 role 候选在回放时不是找不到元素,而是直接抛 Unknown role —— 整步都执行不了,错误信息还容易把人带到别的方向。

所以显式 role 不在标准集合里时就不生成 role 候选,回落到其他策略。

同一类问题还有一个变体:antd / element-plus 的 checkbox、radio 外面包了一层 <label>,能点中的其实是包裹层,但包裹层没有 role 映射。只按包裹层生成就会落到 css,并且把点击后的交互态类编进去。现在的做法是从内部那个唯一的 input 取 role 和可访问名,补一个 role 候选。

录制页:录制产物会被解析成结构化步骤,策略之后可以逐条改

四、弹层元素:自动锚定作用域,而不是靠序号

弹窗、下拉、抽屉一般通过 portal 渲染到 body 下,位置会漂移 —— 靠 nth 序号定位弹层里的元素基本等于抽奖。

所以元素在弹层容器内时,候选会自动带上作用域:先唯一命中容器,再在容器内定位,CSS 也相对容器生成,并且不再生成绝对 xpath。

容器描述的顺序是:优先 role="dialog",其次是容器自身唯一的 class css 或 #id;如果找不到唯一描述,就不生成作用域(一个不唯一的父范围,会让「在容器内定位」变成另一种不确定)。

作用域也能手工干预:步骤表里可以添加/编辑/折叠/删除作用域,role 策略能同时编辑 role 和无障碍名,还有一个「瞄准」图标可以只拾取容器、保留原来的目标定位器。

一个容易踩的实践点:portal 下拉挂到 body 时,不能用视觉上的弹窗当范围 —— 父范围必须是目标真实的 DOM 祖先,同时必须唯一。另外作用域不支持嵌套,也不用于接口断言和页面滚动。

五、拾取时的交互:为什么是 Alt+ 点击

拾取不是「点一下就扣下元素」。实际交互是这样(页面顶部状态栏的原话):

拾取元素:普通点击可正常展开页面;按住 Alt 点击要拾取的元素,再点「确认拾取」完成 · Esc 取消

  • 悬停高亮 + 预览:鼠标划过时,顶部实时显示「将拾取:role=button name=登录」这样的候选摘要;
  • 只有 Alt+ 点击才被当作拾取:这一次点击在捕获阶段被拦下,页面收不到,因此不会有副作用;普通点击照常操作页面 —— 你可以先把弹窗展开、再拾取弹窗里的元素;
  • 确认才落库:待确认时点「确认拾取」才写入结果;Esc 先撤销待确认,再按一次取消整个拾取;
  • iframe 内的元素不支持拾取,会提示在主文档中选择。

这里有一条值得单独说:预览栏显示的就是最终会落库的定位器。它不是页内估算,而是把候选交给后端用真实 Playwright 验一遍(匹配数 === 1,且命中的就是你要的那个节点),再把结论回写到预览栏。你在按「确认拾取」之前看到的,已经是验证后的结果。

验证里还有一个为 SPA 准备的降级:目标元素如果在你点击之后被重渲染替换了(引用脱离文档),只要 URL 没变,就按唯一性采信;跨导航不降级 —— 那种「唯一」很可能命中的是另一个页面上的同结构元素。

六、插件:组件感知的候选,优先但要同样验证

对 Ant Design / Element / Vant 这类组件库,还有一类组件感知候选:由插件贡献,按「组件库 × 能力域」拆成细粒度插件(下拉、树选择、级联、日期、时间、滑块……),内置预设带了 48 个成员插件。

它们插在通用候选前面(通常质量更高),但仍然要走同一套验证:唯一 + 命中同一节点。谁都不能绕过规则。

插件与预设:组件感知候选由成员插件贡献

七、几个我们明确的「不做」

  • 不做 iframe 内元素的拾取:跨 frame 的作用域、序号、导航语义都要单独处理;与其给一个半可靠的定位器,不如明确告诉你「请在主文档中选择」。
  • 不硬凑唯一候选:确实生成不出来时,会明确报「未能生成唯一可用的定位器(已检查:testid=…,role=…,css=…)」,让人换元素或手工指定。
  • xpath 只在主文档、且不在弹层内生成:它对结构变化最敏感,只配当兜底。
  • 拾取会话 10 分钟 TTL,窗口一关算取消,免得留下无人认领的浏览器进程。
  • 自愈不静默回写:定位器失败时模型会提替代方案,但要人来确认是否回写。我坚持这条 —— 静默换定位器等于悄悄降低测试的确定性,某天脚本绿了,其实它已经不再校验你关心的那个元素了。

运行日志:每一步和断言的结果都在这里,定位器失效时从这里开始查

八、想听听你们的做法

我们把「定位策略排序 + 排除瞬态因素 + 弹层锚定作用域」当作标准动作,但定位器这件事各家差异很大。见过做得更好的:后端在 CI 里跑一次定位器体检(对每个定位器验证唯一性,随代码提交回归)、开发在提测前跑一遍 testid 覆盖率检查、以及把定位器和页面结构一起纳入 Code Review。

你们团队现在怎么管定位器?有没有「当时觉得稳、三个月后集体翻车」的经历? 欢迎在评论区聊聊。

现状与限制

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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册。