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 はガード付きのしきい値のもとで、実行時に1つのセレクターを修復します。一方、selector-drift-recovery は新しい DOM に対してオフラインで N 個のセレクターを再生成し、PR にまとめます。test-reliability で2つ目のワークフローを実行する場合は、こちらに切り替えてください。
クイックガイド
| 状況 | 移動先 |
|---|---|
| リファクタリングではない、壊れたテストが1件 | 中止してください。代わりに test-reliability を使用します |
| 複数ファイルにわたって壊れたセレクターが200件以上 | まず領域ごとに分割し(ページ/ディレクトリごとに1つの PR)、その後 Phase 1 へ |
| リファクタリングによって構造だけでなくフロー/セマンティクスも変更された | 中止してください。playwright-automation を使って仕様から書き直します |
| フレームワークの切り替え(Selenium → Playwright) | 中止してください。ドリフト復旧ではなく、test-migration を使用します |
| 旧 DOM の参照がどこにもない | 取得します(Phase 1)。または対象を絞ります。参照がなければ、これは「テストの書き直し」になります |
| 旧 DOM と新 DOM があり、対応付けを始められる | Phase 1 → 6 |
調査時の質問
まず .agents/qa-project-context.md を確認してください。ファイルが存在する場合は、その内容を使い、そこで回答済みの項目は省略します。このファイルには、E2E フレームワーク、セレクター戦略、既知の不安定な領域が記載されています。続いて、次の項目を確認します。
- ドリフトのきっかけは何ですか? 計画済みのリファクタリング(Storybook でマージ前に新しい DOM を確認できる)、リリース済みの再設計(新しい DOM が main にある)、依存関係のアップグレード、それとも Tailwind/CSS の移行ですか?きっかけによって、事前に実行するか、CI の失敗に対応して実行するかが決まります。
- 影響範囲はどのくらいですか? 単一コンポーネント、1つのページ、またはアプリ全体ですか?単一コンポーネントの場合は、そのコンポーネントに触れるテストファイルに復旧範囲を絞ります。全体に及ぶ場合は、半日から1日を見込み、再選択ではなく書き直すべきテストを決めます。
- 現在のセレクター戦略は何ですか? セレクターの大半が
data-testidで、リファクタリング後も testid が維持されているなら、復旧は簡単です。CSS クラスや XPath ベースの場合は、30~60%で新しいロケーターだけでなく、新しい戦略が必要になると想定してください。 - 合格済みのベースラインはありますか? 旧 DOM がどこかに必要です。以前の CI トレースアーティファクト、デプロイ済みのステージングビルド、Storybook のストーリー、またはコンポーネントの Git 履歴が該当します。旧 DOM の参照がなければ、「テストの書き直し」になります。
- 壊れたロケーターはテスト内に直接記述されていますか、それとも Page Object でラップされていますか? JSON レポーターの
error.locationは失敗した行を示します。ロケーターが直接記述されている場合はそのロケーター自体、POM でラップされている場合はテストではなく POM ヘルパーを指します。自動抽出された行番号を信頼する前に、この点を確認してください(「失敗モード」を参照)。 - 作成された PR は誰がレビューしますか? 信頼度スコア付きの更新には、人間による承認が必要です。PR をテスト作成者、リファクタリングを行ったエンジニア、QA リードの誰に送るか、あらかじめ決めてください。
基本原則
-
復旧はイベント駆動であり、失敗駆動ではありません。 リファクタリングを計画しているとき、または完了した直後に実行してください。テストが1つ不安定になったときではありません。テストが1つ壊れた場合 →
test-reliability。同じイベントが原因で10個以上壊れた場合 → このスキルです。 -
古い DOM、新しい DOM、対応付けたペア。 このスキルは、リファクタリング前と後のスナップショットを1つずつ用意することを基盤としています。それ以外はすべて、そのペアを取り巻く記録作業です。スナップショットは生の HTML ではなく、aria スナップショット(
await page.locator('body').ariaSnapshot())として取得してください。ロールツリーの差分こそが、ロール優先の復旧に必要なシグナルであり、生の HTML の差分では埋もれてしまう見た目上の変更(クラス名の変更、ラッパーの div など)を無視できます。両方のスナップショットを用意できない場合は、まずそれを解決してください。 -
常にロール優先で置き換えます。 古いテストで CSS を使っていた場合でも、再生成するセレクターでは
getByRoleとアクセシブル名を優先し、次にフォームフィールドにはgetByLabelを使い、リファクタリングで追加されている場合はgetByTestIdを使います。復旧 PR は、セレクターの平均安定性スコアを引き上げる機会です(以下の0~5の評価基準はtest-reliabilityと共通です)。 -
レイアウトセレクターではなく、領域のスコープ指定で曖昧さを解消します。 ロールと名前だけでは曖昧な場合(「Submit」ボタンが2つある場合など)は、
getByRole('region', { name }).getByRole('button', …)またはgetByRole(...).filter({ hasText })で絞り込みます。:near()/:right-of()は使わないでください。詳細は「避けるべき事項」を参照してください。スコア3の候補とは、領域のスコープ指定を適用し、ロケーターが要素1つだけに一致するようになったものです。 -
1つの PR にファイルごとにまとめ、変更ごとの根拠を添えます。 30件のコミットに分散した47個のセレクター変更を、レビュー担当者が目視で確認することはできません。1つの PR にまとめ、テストファイルごとに変更箇所をグループ化し、変更ごとに信頼度スコアと DOM のスクリーンショットを添付してください。
-
マージ前にテストスイートをパスさせ、不要なテストは削除します。 実行できない再生成済みセレクターは、元の問題より悪い状態です。このスキルは生成された差分ではなく、CI がグリーンになるところまでが対象です。また、リファクタリングで機能が削除された場合は、そのテストも削除してください。存在しなくなった要素のセレクターを再生成してはいけません。
避けるべき事項: 曖昧さの解消に Playwright のレイアウトセレクター
:near()、:right-of()、:left-of()、:above()、:below()を使うこと。これらは公式に非推奨とされ、「削除される可能性」があります。レイアウトが1pxずれただけで一致対象が変わるためです(Playwright のドキュメント、2026年)。また、これらはロール優先の方針にも反します。代わりに、領域のスコープ指定またはgetByRole().filter()を使ってください。
ワークフロー
6つのフェーズがあり、次のフェーズに進む前に各フェーズのチェックを通過する必要があります。
フェーズ1 — 古い DOM のスナップショットを取得する
影響を受けるテストが操作するすべてのページまたはコンポーネントについて、リファクタリング前の状態のスナップショットが必要です。優先順位の高い取得元:
- 最後に成功した CI のトレース成果物。 多くのチームでは、失敗時に Playwright のトレースを保存しています(
trace: 'on-first-retry')。成功した実行の成果物をダウンロードし、npx playwright show-trace traces/checkout.zipでトレースを開いてアクションを選択し、各画面のアクションごとの DOM スナップショットパネルを確認します。(ビューアーには「このステップの HTML をコピー」というメニュー項目はもうありません。スナップショットパネルを確認するか、プログラムで出力する場合はpage.content()/ariaSnapshot()を使って再生してください。) - リファクタリング前のコミットの Storybook。
git checkout <PRE_REFACTOR_SHA>を実行し、Storybook を起動して、小さなpage.content()/ariaSnapshot()スクリプトで各ストーリーを出力します。 - 旧バージョンのままのステージングビルド。 同じフローを操作してスナップショットを取得します。
- コンポーネントの 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 レポーターを指定して新しいビルドに対して実行し、テストプロセスの終了ステータスを保持します。後続の再試行が成功した場合も含め、失敗またはタイムアウトした試行をすべて、プロジェクトと再試行の情報とともに保持してください。中断された試行と実行レベルのエラーも、別途トリアージできるよう保持します。失敗の完全な一覧は、壊れたセレクターの一覧ではありません。
- 証拠に基づいて分類します。 ロケーターのタイムアウトは、アプリケーション、認証、ネットワーク、または準備状態の問題によって発生することがあります。ロケーターが見つからないためにアサーションがタイムアウトする場合もあります。
TimeoutErrorもexpectも、ドリフトを証明したり否定したりするものではありません。 - 不明なフィールドは明示的に不明のままにします。 レポーターが提供するのはテスト/結果の証拠であり、検証済みの編集位置、ロケーターの意図、ページのルートではありません。トレース、実際のテスト/POM ソース、旧アプリと新アプリの状態を確認するまで、これらのフィールドは
nullのままにしてください。近くにあるテストタイトルやデフォルトの/ルートで、不足しているコンテキストを補うことはできません。 - 試行と編集を分けます。 再試行の証拠をすべて保持したうえで、ソース内の各該当箇所について、レビュー済みの復旧対象を 1 つ選択します。未解決の試行や別の原因による試行も、一覧と復旧 PR に残してください。
コンテキストを推測せずに .drift-recovery/failure-inventory.json を書き出す、純粋なコレクターと identify-drift.ts ドライバーについては、references/recovery-scripts.md を参照してください。
ゲート: ファイル/行、正確な旧ロケーター、意図、ページのルート、トリアージの証拠を確認したうえで confirmed-selector-drift と記録された対象のみ、フェーズ 4 に進めます。まず requireVerifiedDrift(target) を呼び出してください。フィールドを埋めただけでは、編集後もテストの意味が保たれることの証明にはなりません。
Phase 4 — 置換候補を生成する
各行について、新しい DOM スナップショットをもとに候補を生成し、共通の 0~5 の評価基準(test-reliability と共有)でそれぞれを採点します。優先順位は以下のとおりです。
- リファクタリングチームが追加した新しい
data-testid— チームが選んだ方法の中で最も安定しています。スコア 5。 getByRole+ アクセシブル名で、ページ内で一意。 スコア 4。- フォームフィールドには
getByLabelを使用します。入力欄が目的で、ラベルが存在する場合に適用します。スコア 4(フィールドにほかの名前がない場合は、単独のロールよりこちらを優先します)。 - ロール + 名前を使い、一致が 1 つだけになるよう領域をスコープします。 ロール + 名前だけでは複数の要素が返る場合は、
getByRole('region', { name }).getByRole(role, { name })または.filter({ hasText })で囲み、スコープ適用後にロケーターが正確に 1 つに一致することを確認します。スコープ適用によって曖昧さが解消された後に限り、スコア 3 を付けます。 - 表示テキストのみ(
getByText)。スコア 2 — 文言の変更に弱いためです。 - 変更された構造上の CSS クラス。 スコア 1 — 通常は引き続き壊れたままです。
- 安全な置換候補なし。 スコア 0 — 人による確認が必要です。
| スコア | 置換戦略 | 自動適用? |
|---|---|---|
| 5 | 新しい data-testid が存在する | はい |
| 4 | getByRole + アクセシブル名(または getByLabel)で、ページ内で一意 | はい |
| 3 | getByRole + 名前を使い、領域をスコープして一致が 1 つのみ | はい |
| 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 は自動適用しません。
Phase 5 — 適用、検証、反復
- スコア 3 以上の置換をフィーチャーブランチに適用します。コンテンツ全体の文字列置換ではなく、
(file, line)を基準に置き換えてください。レポーターのロケーター文字列は レンダリングされた 形式(locator('.summary > h2'))であり、ソース式と完全に一致することはほとんどありません。また、単純なString.replaceは最初の出現箇所しか置き換えず、同一のロケーターがあると衝突します。TypeScript AST の編集、または内容を確認したうえでの限定的な変更を使用し、対象となる古いソースが正確に残っていることを確認してください。同じ行にあるレシーバー、チェーン、その他のロケーターは保持します。行番号だけを使う方法や、最初の閉じ括弧で終わる正規表現は、安全なソース位置指定ではありません。実際に書き込んだ候補に限り、applied: trueに設定してください。 - 以前失敗したテストだけでなく、影響を受けるテストスイート全体を実行します。新しいセレクターが意図しない要素に一致し、以前は成功していたテストを壊す可能性があります。
- テストごとに、成功した場合は置換を維持します。失敗した場合はその 1 行のみを元に戻し、そのテストに人による確認が必要であることを示します。
- 集計結果を出力します。自動で復旧した数を 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 を一度のレビューで確認できること。大きすぎる場合は、領域ごとに分割します(ページ/コンポーネント/テストディレクトリごとに 1 件の PR)。
アンチパターン
-
スコア 0、-1、または -2 の候補を自動適用する。 スコア 2 は「何らかの要素が見つかった」という意味です。これは復旧ではなく賭けです。また、復旧後にスイート全体の安定性スコアが 低下する 主な原因でもあります。自動適用するのはスコア ≥ 3 の候補だけです。
-
曖昧な複数マッチを「スコア 3」と見なす。
getByRole(...)が複数の要素を返す場合、リージョンでスコープを絞り込んで 1 つだけになるまではスコア 3 ではありません。これをスコア 3 として自動適用すると、誤った要素に解決されるロケーターが取り込まれます。 -
スクリーンショットを確認しない。 ページ内に同じロールと名前を持つリージョンが 2 つある場合、スコア 4 の候補でも誤った要素を指すことがあります。スクリーンショットと元のビジネスアサーションを、以前の動作と比較してください。スクリーンショットや信頼度スコアだけでは、意味的に同等であることを確認できません。
-
行を基準にした編集ではなく、コンテンツ全体を対象に文字列置換する。
content.replace(oldLocator, …)は最初の出現箇所しか置換せず、重複するロケーターと衝突し、レポーターが出力した文字列とソース式が異なる場合は何も変更せずに終了します。特定の(file, line)を編集してください。 -
POM でラップされたロケーターについて、自動抽出された行番号を信用する。 JSON レポーターの
error.locationが示すのは失敗した行です。ページオブジェクトの場合、テストではなくヘルパーが示されます。適用前にトレースアクションからロケーターを読み直すか、POM ソースを grep してください。 -
削除された機能のテストを復旧する。 リファクタリングでフローが削除されている可能性があります。フェーズ 2 で削除されたルートを特定し、該当するテストを削除してください。存在しなくなった要素のセレクターを再生成しないでください。
-
CI で復旧 PR を自動マージする。 PR 自体 が成果物です。変更ごとの根拠を人間が確認することが目的です。テストに合格したら自動マージするのでは、その目的が損なわれます。誤っているものの存在はする要素を指すセレクターでも、スイートが成功すれば通過し、信頼を損ないます。レビュアーを必須にしてください。
-
PR を緊急扱いする。 スイートの失敗は緊急に感じられますが、大切なのは 正しく 復旧されていることです。時間に追われると、スイートをひそかに劣化させるスコア 2 の置き換えにつながります。
失敗モード
| 症状 | 考えられる原因 | 修正または確認 |
|---|---|---|
CI が赤なのに identify-drift.ts が失敗を 0 件検出する | JSON レポートが出力される前にスイートでエラーが発生した、または誤ったファイルを解析した | jq '.stats' .drift-recovery/results.json を実行し、--reporter=json の出力先がこのファイルになっていることを確認します |
| 抽出された行がテストではなく POM ファイルを指している | ロケーターがページオブジェクトにラップされている | トレースアクションからロケーターを読み取るか、出力された文字列を POM ソースで grep します |
generate-candidates.ts が intent/route に対して undefined を読み取る | フェーズ 3 の出力に、推定された intent/ページ route のフィールドがない | フェーズ 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 に組み込みます。