一、背景一个要兼容两套系统的录制引擎我们的测试平台需要把人工操作录制为 Playwright 用例。挑战在于老系统是标准 Element UI 组件库el-select-dropdown__item、el-date-table新系统大量使用自定义组件el-popover弹窗、expand-container折叠容器且页面存在大量display:none的隐藏副本节点脏数据。最初版本能录制、能点但执行时频繁定位失败最终定位到三类系统性问题问题表现根因脏数据选择器匹配多个元素 → strict mode 报错隐藏副本节点参与匹配多实例容器img:nth-of-type(2)匹配 6 个6 条记录结构相同位置选择器失效动态属性[placeholder货到付款]频繁失效placeholder 是选项名随数据变化二、总体架构录制端 执行端双保险重构的核心思路是把准前置到录制端把稳兜底在执行端录制端capture_injector.js smart_record.py 多策略生成 → JS 预校验 → Playwright 后校验 → 优先级排序 → 存储 执行端code_generator.py 按 selectorType 分派 → :visible 过滤 → strict mode/Timeout 降级 → AI 兜底三、选择器策略体系10 种策略各有置信度录制时对每个元素生成 10 种候选选择器每种带基础置信度策略示例基础置信度id / data-testid#form-1230.95name[namedeliveryAddress]0.85form-label//label[text()付款方式]/following-sibling::div//input0.82placeholder[placeholder货到付款]0.80texttext采购合同0.75filteredspan:has-text(采购合同)count 打折parent-tag / nth / tagdiv.el-input input极低关键点置信度是先验分数最终由 Playwright 实时 count 决定后校验。count1 才 uniquecount 越大 confidence 打折越狠。四、选择器优先级业务语义 位置这是重构中最有价值的设计。select_best_selector的排序规则1. preUnique 匹配目标预校验唯一且命中 2. post_unique后校验 count1 3. form-label//label 业务标签定位即使 count1执行端 :visible 过滤 4. preCount 0按 confidence 5. count 升序兜底匹配越少越精确核心转变从位置定位优先nth-of-type、parent-tag转向**业务语义定位优先**label 文本、容器文本。位置选择器在动态列表页几乎必然失效而付款方式这个标签文本几乎不会变。五、三类经典问题的解法5.1 脏数据:visible:not([disabled])过滤新系统页面存在display:none的隐藏副本如审核按钮旁总有一个隐藏的反审核。解法分两层录制端JS 预校验时过滤display:none的匹配项执行端对 XPath / 属性 /:has-text类选择器自动拼接:visible:not([disabled])javascriptawait page.locator(sel).and(page.locator(:visible)) .and(page.locator(:not([disabled]))).click();5.2 多实例容器容器文本上下文定位6 条采购记录共用div.expand-container结构img:nth-of-type(2)匹配 6 个。位置信息在动态列表里天然不唯一改用容器内业务标题定位xpath//div[contains(class,expand-container)][contains(.,结算信息)]//img该 XPath 匹配 4 个含隐藏但:visible过滤后恰好 1 个 → 稳定命中。录制端对应生成 form-label 的contains变体xpath//*[contains(.,结算信息)][ancestor::div[contains(class,expand-container)]]//img5.3 动态 placeholder回归 label 定位下拉框的 placeholder 是选项名货到付款、赊购随业务数据变化[placeholder赊购]极不稳定。改为无后缀的 label 定位xpath//label[text()付款方式]/following-sibling::div//input ✅ //label[text()付款方式]/following-sibling::div//input[placeholder赊购] ❌六、执行端兜底绝不裸奔即使选择器万无一失页面状态仍可能变化。执行端加了三级降级javascripttry { await page.click(sel, {...}); } catch (e) { if (e.message.includes(strict mode)) { // 多匹配 → 点第一个可见的 await page.locator(sel).and(page.locator(:visible)).first().click(); } else if (e.message.includes(Timeout)) { // 0 匹配 → 去掉 :nth-of-type 再试 const fb sel.replace(/:nth-of-type$\d$/g, ); await page.locator(fb).and(page.locator(:visible)).first().click(); } }七、踩过的坑都是真金白银f-string 生成 JS 代码括号失衡多嵌套 try-catch 时{{ }}极易出错复杂逻辑必须先提取为 Python 变量再插入否则生成文件直接 SyntaxError。.first()盲点可能触发危险操作展开图标.first()点到隐藏副本后浏览器直接window.close()。降级必须配合:visible过滤且优先业务语义选择器而非位置选择器。preCount 假信号Python 生成的策略被错误填充 JS 预校验值产生假的preUnique导致排序误判。策略来源必须区分JS 预校验 vs Python 后校验。旧数据兼容page.click()是严格模式改过滤条件会改变匹配集可能把报错变成点错。改造必须按 selectorType/录制来源区分已存用例零影响。八、总结这次重构最大的收获不是更准的选择器而是一套工程化方法论多策略 置信度 实时校验让数据说话而不是靠直觉挑选择器业务语义 位置label 文本、容器标题比 nth-of-type 可靠一个量级录制端 执行端双保险录制端保证生成的就是对的执行端保证对了的不会崩兼容旧数据是硬约束任何改动都要评估对存量用例的影响选择器工程化的本质是把人工挑选择器的经验沉淀为可量化的决策规则——这也是录制工具从玩具走向生产力的分水岭。