那次自动发布前,我只想完成一个很小的设置:让内容不显示位置。页面看上去并不复杂,正文编辑区里有一段文字,短标题也在附近,位置栏则是另一个可以展开的表单项。问题出在正文里恰好出现了一个城市话题。
旧逻辑的思路很直接:在整页辅助树里搜索城市文本,找到第一个匹配节点,就把它当成位置控件。结果它先点中了正文里的话题,而不是位置栏中的选择项。随后位置校验失败,安全机制阻止了真实发布,所以错误没有带着错误位置发出去。但这次失败仍然值得认真处理,因为它暴露的不是一个城市词写得不够多的问题。
自动化误点的危险,往往不在于没找到文字,而在于找到了同样的文字却没理解它属于哪一层。
页面里有相似文字,不代表它们是同一种对象
我一开始以为,既然目标是某个城市,就把城市名搜准一点,或者把更多城市加入关键词列表,应该就能解决。后来回看页面结构,才发现这个判断从一开始就偏了。
同一页里可能同时出现正文话题、短标题和位置栏。它们都能被辅助树读成文字节点,但各自承担的功能完全不同。正文中的城市是内容,短标题中的城市可能只是摘要,位置栏里的城市才是发布设置。文本相同,只说明它们的显示结果相似,并不说明它们可以互相替代。
如果自动化只看文字,不看所在容器,那么页面一旦增加一段说明、一个推荐话题,或者调整控件顺序,匹配顺序就可能改变。最先出现的节点也许永远不是目标。这样的逻辑在静态演示里容易通过,在真实页面里却会随着内容变化而漂移。
这件事让我重新意识到,辅助树不是一张写满答案的清单。它把页面暴露出来,至于某个节点能不能执行某个动作,还要结合它的父级关系、控件角色和所处区域来判断。
表面上是关键词问题,实际上是作用域问题
旧逻辑失败后,最容易误判的地方,是把“匹配到了错误节点”理解成“关键词覆盖不够”。继续扩充城市关键词列表,或者把第一个匹配文本当成目标控件,都是错误捷径。它们只会让搜索更忙,却不会让目标更清楚。
我选择先定位结构容器,再在容器内做文本匹配,是因为位置栏本身有可识别的边界:它属于包含位置选择框的表单项。先找到这个表单项,候选范围就从整页缩小到一个功能区域;之后再判断位置标签和位置名,文字才开始有了上下文。
当多个节点共享同一段文字时,正确的定位顺序应该是先判断“它在哪个功能容器里”,再判断“它显示了什么”。
这不是为了让代码看起来更复杂,而是为了把业务判断放回页面结构中。位置设置的目标不是“页面上出现某个城市”,而是“位置表单项中存在可操作的对应选项”。两句话读起来相近,实际保护的边界完全不同。
先缩小候选,再排除会混淆的区域
结构容器确定以后,我又补了一层排除规则。正文编辑区不能成为位置候选,输入框不能直接当作位置标签,话题节点也不能因为文字相同就被接受。它们都可能出现在位置表单项附近,甚至可能共享某些可访问名称,但功能角色不同。
这里的“排除”很重要。很多自动化逻辑只规定什么可以匹配,却没有规定什么一定不能匹配。页面越复杂,允许条件越宽,误点的机会越多。把正文、输入框和话题节点明确排除,相当于给候选集合加了一道负向边界,让结果不只是“像目标”,还必须“不是这些已知的干扰物”。
如果不处理这一层,修复可能只在当前页面有效。今天正文话题排在前面,修复人员把位置节点调到搜索结果第一位;下一次正文顺序改变,或者编辑器增加一个隐藏输入框,问题仍会回来。可靠定位不是把正确节点推到第一个,而是让错误节点从候选集合里失去资格。
我也保留了失败即停止的处理方式。如果结构容器找不到、候选超过预期,或者位置标签和位置名无法同时确认,就不继续点击,也不尝试猜一个最接近的结果。自动发布最需要保守的地方,正是会改变外部状态的最后一步。
校验失败时,停下来本身就是结果
这次真实页面没有因为误点而发布出去,靠的是后面的校验。它没有把“已经点击过”当成成功,而是继续检查位置状态是否符合要求。发现状态不对后,流程停止了真实发布。
我不把这看成一次偶然的幸运。发布前的校验是第二道边界,定位器负责找到正确对象,状态检查负责确认动作真的作用在正确对象上。前一道边界漏掉误点,后一道边界仍然有机会阻止外部影响。
如果没有这个 fail-closed 处理,错误位置可能直接随内容发出,之后再修改就不只是修一个定位器,而要处理已经产生的公开内容。对外部发布来说,无法确认正确,应该等同于暂时不能发布,而不是自动选择一个“看起来最像”的节点。
这也改变了我对自动化速度的看法。少一次确认,可能让流程在顺利时快一点;但一旦候选发生歧义,省下来的几秒会变成撤回、修正和追查的成本。保守并不等于低效,它是在把不可逆的风险挡在边界之外。
用复现页面把误点写成回归测试
修复之后,我没有只在真实页面上重新点一遍。真实页面的内容、顺序和异步状态都可能变化,仅凭一次成功不能证明定位逻辑已经稳定。所以我准备了一个足够小的模拟页面,把这次误点的条件保留下来:正文里出现城市话题,页面同时有短标题和位置栏,位置项中则包含位置标签与位置名。
测试先验证旧问题会被覆盖,再验证新规则不会把话题当成位置。它检查正文编辑区和输入框被排除,位置标签和位置名仍然能够保留为候选,并确认候选最终落在包含位置选择框的表单项内部。这样测试的就不是某个固定文本,而是这次修复真正要守住的结构关系。
我选择模拟页面,是因为它能把页面内容和验证结果固定下来。它不需要复刻整个发布系统,也不需要依赖真实身份或真实发布;只要能稳定重现“相似文字出现在错误区域”这一条件,就足以证明定位器是否按预期收窄范围。
这件事让我重新意识到,回归测试的价值不只是防止代码再次报错,更是把一次人工发现的危险场景保存下来。没有测试,下一次维护者可能只看到“把定位改成了表单内搜索”,却不知道为什么要排除话题和编辑区。有了复现页面,那个理由就不再依赖记忆。
方法不是多找文字,而是留下判断链
把这次处理抽象成方法,大致可以分成一条很短的流程。先判断目标控件属于哪个结构容器,再把搜索限制在这个容器内;接着根据控件角色和状态筛选候选,同时排除正文、输入框和话题节点;如果结果不唯一或状态不符合,就保持停止;最后用一个能复现相似文本冲突的页面做回归,并在真实发布前只跑 dry-run。
这里最值得复用的不是某个选择器写法,而是判断顺序。先结构,后文本;先排除风险区域,再接受候选;先验证状态,后允许外部动作。顺序一旦反过来,后面的规则都会变成给错误候选打补丁。
我更愿意把它看成一种工作习惯,而不是一个技巧。网页会改版,正文会变化,控件名称也会调整,但“目标属于哪个容器”“哪些区域不能碰”“失败时能不能安全停下”这些问题一直存在。把它们写进定位和测试,自动化才不会只依赖当前页面的偶然排列。
下次我会怎么做
下次遇到自动发布页面里的相似文本,我会先问:我要找的是一段文字,还是一个承担特定功能的控件?如果答案是后者,就不会从整页开始搜索,而会先找对应的表单项或结构容器。
然后我会列出最容易混淆的区域。正文、编辑器输入框、话题节点,以及任何会随用户内容变化的区域,都应该先考虑是否需要排除。候选越接近外部动作,越不能靠“第一个匹配”这样的默认行为收尾。
在真正发布前,我会保留状态校验和 dry-run。只要容器、候选或最终状态有一项无法确认,就让流程停在发布之前。最后,把这次误点条件放进一个小型复现页面,确认话题会被排除、位置标签和位置名会被保留。
一个流程成熟的标志,不是它能在今天的页面上点对一次,而是页面内容变化后,它仍然知道哪些地方不能点。
这次的问题表面上是自动化找错了文字,实际上是在提醒我:页面上的文本只是线索,结构和边界才是判断。把判断链留下来,下一次就不必靠运气从整页里寻找一个看起来相同的答案。