关于 selector-drift-recovery
一项用于在计划内重构或改版后恢复 Playwright 选择器的技能。它对比新旧 DOM 快照,生成优先使用角色定位的候选项并评分,最终将经过验证的修改和证据整理到可审查的 PR 中。
主要功能
- 01对比新旧 DOM 快照
- 02盘点并核实失败尝试
- 03生成角色优先的选择器候选项
- 04为候选项评分并附上截图
- 05在可审查的 PR 中验证修改
适用场景
- 01网站改版后修复选择器
- 02组件重构后更新测试
- 03带证据审查多项选择器修改
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill selector-drift-recovery安装后在 Claude Code 中输入/selector-drift-recovery确认已加载。
附带文件references/recovery-scripts.mdreferences/score-candidates.mjs
详细介绍
这是 test-reliability 的批量、离线处理对应方案。两者共用多属性定位器和置信度评分基础机制,但运行方向相反:test-reliability 在运行时通过受控阈值修复单个选择器;selector-drift-recovery 则针对新 DOM 离线重新生成 N 个选择器,并将它们打包成一个 PR。如果你正在 test-reliability 中执行第二种工作流,请切换到这里。
快速路径
| 情况 | 前往 |
|---|---|
| 一个测试损坏,并非重构所致 | 停止——改用 test-reliability |
| 多个文件中有 200 个以上的选择器损坏 | 先按区域拆分(每个页面/目录一个 PR),然后执行阶段 1 |
| 重构改变了流程/语义,而不只是结构 | 停止——使用 playwright-automation 根据规范重写 |
| 切换框架(Selenium → Playwright) | 停止——使用 test-migration,而非漂移恢复 |
| 完全没有旧 DOM 参考 | 获取一份(阶段 1),或缩小范围——没有它就相当于“重写测试” |
| 已有新旧 DOM,准备开始映射 | 依次执行阶段 1 → 6 |
发现问题
先检查 .agents/qa-project-context.md——如果存在,就使用其中的信息,并跳过已回答的问题。该文件会指出你的 E2E 框架、选择器策略和已知脆弱区域。然后:
- 是什么触发了漂移? 是计划中的重构(可通过 Storybook 在合并前查看新 DOM)、已发布的重设计(新 DOM 已进入 main)、依赖项升级,还是 Tailwind/CSS 迁移?触发因素决定你是提前运行恢复,还是响应 CI 失败。
- 影响范围有多大? 涉及单个组件、一个页面,还是整个应用?单个组件:将恢复范围限定为涉及该组件的测试文件。全局范围:预留半天到一天,并决定哪些测试应该重写,而不是重新选择定位器。
- 当前采用什么选择器策略? 如果选择器主要是
data-testid,且重构保留了这些测试标识,恢复会很简单。如果基于 CSS 类或 XPath,预计有 30–60% 的选择器需要采用新策略,而不只是换一个定位器。 - 是否有通过测试的基线? 你需要在某处找到旧 DOM:之前的 CI 跟踪产物、已部署的预发布构建、Storybook 故事,或组件的 Git 历史记录。没有旧 DOM 参考,就会退化为“重写测试”。
- 失效的定位器是内联的,还是封装在 Page Object 中? JSON 报告器的
error.location会指向失败行。对于内联定位器,它指向定位器本身;对于由 POM 封装的定位器,它指向 POM 辅助函数,而非测试。在信任自动提取的行号之前,务必确认这一点(参见“失败模式”)。 - 由谁审查最终 PR? 带置信度评分的更新需要人工签字批准。提前确定 PR 应提交给测试作者、负责重构的工程师,还是 QA 负责人审查。
核心原则
-
恢复由事件驱动,而非由故障驱动。 在计划进行重构或刚完成重构时运行此流程,而不是在某个测试偶尔失败时运行。一个测试损坏 →
test-reliability。同一事件导致十个或更多测试损坏 → 使用此技能。 -
旧 DOM、新 DOM,以及映射后的配对。 整个技能都依赖重构前和重构后的快照。其他所有内容都是围绕这对快照进行的记录工作。请将两者都采集为 aria 快照(
await page.locator('body').ariaSnapshot()),而不是原始 HTML——角色树差异正是以角色为先的恢复所需的信号,也不会像原始 HTML 差异那样被外观变化(类名重命名、包装 div)淹没。如果无法生成这两份快照,请先解决这个问题。 -
每次都优先使用角色进行替换。 即使旧测试使用了 CSS,重新生成的选择器也应优先使用
getByRole+ 可访问名称,其次是表单字段的getByLabel,再其次是重构时新增了测试 ID 的getByTestId。恢复 PR 是提升选择器平均稳定性评分的机会(使用下方的 0–5 评分标准,与test-reliability共用)。 -
通过限定区域消除歧义,不要使用布局选择器。 当角色 + 名称存在歧义时(例如两个“提交”按钮),使用
getByRole('region', { name }).getByRole('button', …)或getByRole(...).filter({ hasText })缩小范围。不要使用:near()/:right-of()——参见“避免”说明。评分为 3 的候选项,是指已应用区域限定,且定位器现在恰好匹配一个元素。 -
一个 PR,按文件分组,并为每项更改提供证据。 审阅者无法逐一检查散布在 30 个提交中的 47 处选择器更改。请将它们整合到一个 PR 中,按测试文件分组代码块,并为每项更改附上置信度评分和 DOM 截图。
-
合并前必须通过测试套件,并删除失效测试。 无法运行的重新生成选择器比原来的问题更糟;技能应以 CI 全绿结束,而不是只生成差异。此外,如果重构移除了某个功能,应删掉对应测试——不要为已不存在的元素重新生成选择器。
避免: 使用 Playwright 布局选择器
:near()、:right-of()、:left-of()、:above()、:below()来消除歧义——这些选择器已被官方弃用,并且“可能会被移除”,因为 1px 的布局偏移就会改变匹配结果(Playwright 文档,2026 年)。它们也与以角色为先的原则相悖。请改用区域限定 /getByRole().filter()。
工作流
分为六个阶段,每个阶段都必须通过检查后才能进入下一阶段。
阶段 1 — 为旧 DOM 采集快照
你需要为受影响测试涉及的每个页面或组件采集重构前状态的快照。来源按优先级排序如下:
- 最近一次通过的 CI 跟踪产物。 大多数团队会在失败时保存 Playwright 跟踪(
trace: 'on-first-retry')。下载成功运行的产物,使用npx playwright show-trace traces/checkout.zip打开跟踪,选择某项操作,并查看每个界面对应的每项操作 DOM 快照面板。(查看器不再提供“Copy HTML at this step”菜单项;请阅读快照面板,或通过page.content()/ariaSnapshot()重放以编程方式导出。) - 重构前提交中的 Storybook。 执行
git checkout <PRE_REFACTOR_SHA>,启动 Storybook,并使用简短的page.content()/ariaSnapshot()脚本导出每个 story 的快照。 - 仍在运行旧版本的预发布构建。 按相同流程操作并采集快照。
- 组件的 Git 历史。 可以重建,但成本最高——需要单独渲染组件。
输出:为每个受影响的单元生成 .drift-recovery/old/<page-or-component>.aria.yml(如果还需要原始标记,则同时生成 .html)。
检查点: 你可以根据快照文件回答“这个页面在测试上次通过时是什么样子?”,而不是凭记忆回答。
第 2 阶段 — 快照新的 DOM
在重构后的构建版本中运行相同的页面区域。最好使用 Vercel/Netlify 预览部署,也可以使用本地开发服务器或 CI 中的 PR 分支。快照前,等待应用实际的水合/就绪契约。网络空闲并不能证明客户端渲染已完成。
输出:与旧集合相匹配的 .drift-recovery/new/<page-or-component>.aria.yml。
门槛: 每个旧快照都必须有对应的新快照。404 是需要调查的问题,并不能证明相关内容是有意删除的。提议移除覆盖范围前,先确认产品变更。
第 3 阶段 — 清点失败尝试,然后验证来源和意图
针对新构建版本运行 JSON reporter,并保留测试进程的退出状态。保留每一次失败/超时的尝试、项目和重试,即使后续重试通过也不例外。保留被中断的尝试和运行级错误,以便单独分流处理。完整的失败清单还不等于损坏的选择器列表。
- 根据证据分类。 定位器超时可能由应用、身份验证、网络或就绪状态问题导致。断言也可能因为定位器不存在而超时。
TimeoutError和expect都不能证明或排除漂移。 - 明确保留未知字段。 reporter 提供的是测试/结果证据,而非经核实的编辑位置、定位器意图或页面路由。检查跟踪记录、实际测试/POM 源码以及新旧应用状态之前,将这些字段保留为
null。附近的测试标题或默认/路由无法补足缺失的上下文。 - 区分尝试与编辑。 保留所有重试证据,然后针对每个源代码中的出现位置,选择一个经过审核的恢复目标。将未解决的尝试和其他原因导致的尝试保留在清单和恢复 PR 中,以便查看。
纯收集器和 identify-drift.ts 驱动程序见 references/recovery-scripts.md。它们会写入 .drift-recovery/failure-inventory.json,而不会臆测上下文。
门槛: 只有记录为 confirmed-selector-drift,且文件/行号、确切的旧定位器、意图、页面路由和分流证据均已核实的目标,才能进入第 4 阶段。先调用 requireVerifiedDrift(target);仅仅填入字段并不能证明编辑保留了测试的含义。
第 4 阶段 — 生成替换候选项
对于每一行,针对新的 DOM 快照生成候选项,并按 0–5 分的评分标准(与 test-reliability 共用)为每项评分。策略优先级如下,越靠前越好:
- 重构团队新增的
data-testid——这是他们做出的最稳定选择。评分 5 分。 getByRole+ 可访问名称,且在页面上唯一。 评分 4 分。- 对表单字段使用
getByLabel,当意图是输入字段且存在标签时。评分 4 分(如果字段没有其他名称,则优先于单独使用 role)。 - role + name,并限定在仅匹配一个元素的区域内。 如果单独使用 role + name 返回的元素超过 1 个,则将其包装起来——使用
getByRole('region', { name }).getByRole(role, { name })或.filter({ hasText })——并确认限定范围后的定位器恰好匹配一个元素。只有在限定范围后结果明确无歧义,才评分 3 分。 - 仅使用可见文本(
getByText)。评分 2 分——容易因文案变动而失效。 - 变更结构上的 CSS 类。 评分 1 分——通常仍会失效。
- 没有安全的替换方案。 评分 0 分——标记为需要人工处理。
| 评分 | 替换策略 | 自动应用? |
|---|---|---|
| 5 | 存在新的 data-testid | 是 |
| 4 | getByRole + 可访问名称(或 getByLabel),在页面上唯一 | 是 |
| 3 | getByRole + name,限定在恰好匹配一个元素的区域内 | 是 |
| 2 | 仅使用可见文本 | 否 |
| 1 | 变更结构上的 CSS 类 | 否 |
| 0 | 未找到安全的替换方案 | 否 — 标记为需要人工处理 |
只有在限定范围后已确定唯一元素时,候选项才可评为 3 分。仍有多个匹配项(count > 1、“需要限定范围”)的候选项不能评为 3 分——它尚未完成,不得自动应用。
保留实时 Locator 对象用于唯一性检查和截图;拟议的源代码表达式不是 CSS 选择器,绝不能对其求值。为名称加引号,并转义正则表达式文本,包括 DOM 属性中的标点和引号。缺少截图证据时,候选项仍未解决。
输出:.drift-recovery/candidates.json,每项提案包含 { file, line, oldLocator, selector, score, rationale, screenshotPath, applied }。如果候选项生成或证据采集失败,或者没有唯一候选项达到最低分,则保留一条评分为 0 的未解决记录。候选项辅助函数参见 references/recovery-scripts.md。
门槛: 每一行都有评分 ≥ 3 且已确认唯一的候选项,或已标记为需要人工审核。评分为 0、1 或 2 的候选项绝不自动应用。
第 5 阶段 — 应用、验证、迭代
- 在功能分支中应用评分 ≥ 3 的替换项。按
(file, line)替换,而不是进行全文字符串替换——报告器中的定位器字符串是渲染后的形式(locator('.summary > h2')),通常与源代码表达式并不完全一致;而简单的String.replace只会命中第一个匹配项,还可能与相同定位器冲突。使用 TypeScript AST 编辑或经过检查的精确小范围修改,并确认原始源代码仍完全匹配。保留接收对象、调用链以及同一行上的其他定位器。单靠行号,或以第一个右括号结尾的正则表达式,都不是安全的源代码锚点。仅当某项候选实际写入后,才将其设为applied: true。 - 运行所有受影响的测试套件,而不只是之前失败的测试——新选择器可能匹配到非预期的元素,导致之前通过的测试失败。
- 对每项测试:通过 → 保留替换。失败 → 还原该行,并将测试标记为需要人工审核。
- 输出摘要:自动恢复 N 项,标记 M 项。
门槛: 已恢复的测试通过测试套件。已标记的测试须清晰标出,不得悄然纳入。
阶段 6 — 提交 PR
PR 就是交付成果。标题: chore(tests): selector recovery after <refactor description>。正文(根据 candidates.json 生成,并筛选 applied 项):
## Trigger
<Link to the refactor PR / describe the redesign>
## Summary
- N test files updated - M selectors changed
- K tests deleted (feature removed) - L tests flagged for manual review
## Per-file changes
<For each file: a table of line, old, new, score, screenshot URL>
## Flagged for review
<Every unresolved source location, including candidates awaiting review, with verified intent or explicit missing context>
## How to review
- Check each screenshot: does `new` point at the element `old` pointed at?
- For score-3 candidates, verify the region scope is meaningful in the new design.
- For flagged tests, decide: rewrite, delete, or accept a manual selector update.
按照团队的 CI 构建产物 URL 格式内嵌附上截图。有关源码编辑检查,请参阅 references/recovery-scripts.md 和 build-pr-body.ts。
门槛: PR 应能在一次评审中完成。内容过多 → 按区域拆分(每个页面/组件/测试目录一个 PR)。
反模式
-
自动应用评分为 0、-1 或 -2 的候选项。 评分为 2 意味着“我们找到了某个元素”。这不是恢复,而是在碰运气——也是恢复后整个测试套件稳定性评分下降的常见原因。只自动应用评分 ≥ 3 的候选项。
-
将存在多个匹配项的情况判为“评分 3”。 如果
getByRole(...)返回多个元素,在通过区域限定将结果缩小到唯一元素之前,评分就不能算 3。将其评为 3 并自动应用,会提交一个指向错误元素的定位器。 -
跳过截图。 如果页面有两个区域具有相同的角色和名称,评分为 4 的候选项仍可能指向错误元素。请对照截图和原有业务断言,确认其与旧行为一致;仅凭截图或置信度评分无法证明语义等价。
-
使用针对整个内容的字符串替换,而不是按行锚定编辑。
content.replace(oldLocator, …)只会命中首次出现的位置,会与重复定位器冲突;当报告器渲染的字符串与源码表达式不同时,还会悄无声息地不执行任何操作。请编辑指定的(file, line)。 -
相信自动提取的 POM 封装定位器行号。 JSON 报告器的
error.location指向失败行;对于 Page Object,这一行属于辅助函数,而不是测试。应用前,请从跟踪操作中重新读取定位器,或在 POM 源码中搜索。 -
恢复已删除功能的测试。 重构可能移除了一些流程。请在阶段 2 中映射已删除的路由,并删去这些测试——不要为已不存在的元素重新生成选择器。
-
在 CI 中自动合并恢复 PR。 PR 本身就是交付物;这么做的全部目的,是让人类逐项查看每项变更的证据。测试通过后自动合并会违背这一目的——即使选择器指向了一个存在但错误的元素,测试套件也会通过,从而损害信任。必须要求评审者审查。
-
把 PR 当作紧急事项。 测试套件失败令人焦急;但更重要的是正确地恢复测试。时间压力会导致用评分为 2 的替代项悄然降低测试套件的质量。
故障模式
| 症状 | 可能原因 | 修复或检查 |
|---|---|---|
尽管 CI 为红色,identify-drift.ts 却找不到任何失败项 | 测试套件在生成 JSON 报告前出错,或者你解析了错误的文件 | 执行 jq '.stats' .drift-recovery/results.json;确认 --reporter=json 已将结果重定向到该文件 |
| 提取的行号指向 POM 文件,而不是测试文件 | 定位器封装在 Page Object 中 | 从跟踪操作中读取定位器,或在 POM 源码中搜索渲染后的字符串 |
generate-candidates.ts 读取到的意图/路由为 undefined | 阶段 3 的输出缺少推断意图/页面路由字段 | 在阶段 3 中填入这些字段——它们不在报告器的输出中;生成器无法推断 |
| 新 DOM 快照缺少客户端渲染的元素 | 在水合前进行了快照 | 在调用 ariaSnapshot() 前,等待应用自身的就绪约定 |
| 恢复后平均稳定性评分下降 | 自动应用了评分为 2 的候选项/CSS 候选项 | 在 candidates.json 中撤销评分低于 3 的候选项;最终只应保留角色/标签/testid 选择器(见下方评分器) |
验证
在打开 PR 之前,先在恢复分支上按从小到大的顺序运行以下命令:
set -euo pipefail
# 1. Check the recorded applied scores and candidate schema.
node references/score-candidates.mjs .drift-recovery/candidates.json
# Exit 0: all recorded applied scores >= 3. Exit 1: a lower score.
# Exit 2: invalid input or no applied edits to score.
# 2. Preserve the test process exit status and the complete affected-suite report.
PLAYWRIGHT_TEST_BASE_URL=$PREVIEW_URL npx --no-install playwright test --reporter=json \
> .drift-recovery/post-recovery.json
jq -e '.stats.expected > 0 and .stats.unexpected == 0 and .stats.flaky == 0' \
.drift-recovery/post-recovery.json
# Account for skipped, blocked and unselected tests separately; do not hide them to get green.
# 3. The PR exists with the evidence body
gh pr view --json title,body -q '.title' # contains "selector recovery"
评分器会检查记录的分数下限和架构,但不会验证定位器的含义、截图内容或测试执行情况。进程成功退出且受影响的测试套件结果非空,可作为记录的构建已执行的证据;在断定恢复成功之前,请检查新旧意图和排除项。 如果未应用任何编辑,请报告评分器无法评分的结果和未解决的工作,而不要声称恢复成功。
完成条件
- 每个范围内的失败都必须有据可查:要么有证据表明已恢复,要么说明原因并标记为未解决。只有在确认产品确实移除了相关功能后,才能删除测试。
- 如果应用了编辑,
references/score-candidates.mjs candidates.json应以 0 退出。必须明确报告缺少输入和未应用任何编辑的情况。 - PR 已打开(
gh pr view可成功运行),并按文件分组,提供每项更改的证据:置信度分数和截图。 - 记录受影响测试套件的完整进程退出情况、构建、计数、重试、跳过项和未解决的失败。只有运行结果支持该结论时,才能声称测试套件通过。
- 在
.agents/qa-project-context.md中添加简短说明,描述此次重构以及引入的任何新测试模式。
参考文件(位于 references/ 中)
- recovery-scripts.md — aria-snapshot 捕获、失败分类、可执行的候选项和证据辅助工具、经检查的源代码编辑说明,以及保留未解决位置的 PR 摘要。包含 Cypress 适配限制。
- score-candidates.mjs — 简易稳定性评分器;输出已应用分数的平均值,以及已应用且分数低于 3 的行数。用于“验证”和“完成条件”。
相关技能
- test-reliability — 运行时逐项测试修复。适用于单个不稳定测试,而非由重构驱动的大规模更新。共用 0–5 稳定性评分标准。
- playwright-automation — 从头编写新测试。当重构移除了足够多的功能,以至于测试应重写而非修补时使用。
- test-migration — 切换框架(Selenium → Playwright)。迁移会重新录制测试;即使两者都会同时影响许多测试,这也不属于选择器漂移。
- visual-testing — 如果每个 PR 都运行此项,重构的视觉差异就会在合并前被发现。当缺少此类覆盖时,恢复流程可作为后备方案。
- ci-cd-integration — 将恢复 PR 的验证步骤接入 CI。