mcpField
P
printing-the-semantics-tree

printing-the-semantics-tree

github.com/skydovesテスト・品質33963 回インストール22 週間前に更新
リポジトリ

printing-the-semantics-tree について

Jetpack Compose UIテストでファインダーが失敗した際に、セマンティクスツリーを調べるためのスキルガイドです。マージ済み・未マージのツリー出力と、カスタム検証用のノード取得を説明します。

主な機能

  1. 01マージ済みセマンティクスツリーの出力
  2. 02未マージツリー内のノード確認
  3. 03ツリー出力とフレームワークのヒントの解釈
  4. 04カスタムマッチャー向けSemanticsNode取得
  5. 05ログ用ツリー出力の文字列化

活用シーン

  1. 01ノードが見つからない・複数一致する問題の調査
  2. 02セマンティクスのマージによるノードの隠れ方の確認
  3. 03カスタムマッチャーでのノード属性検査
  4. 04CIログへのツリー構造の記録

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/skydoves/android-testing-skills --skill printing-the-semantics-tree

インストール後、Claude Code で/printing-the-semantics-treeを入力して読み込まれたか確認します。

詳細

セマンティクスツリーの出力 — ファインダーが失敗したら、ツリーをダンプする

onNodeWithTag の失敗は構造的な問題です。タグが間違っている、マッチャーがマージ済みツリーを対象としているのにタグがリーフに付いている、またはノードがまだ実際にコンポーズされていない、といった原因が考えられます。いずれもタイミングの問題ではないため、Thread.sleep を使っても解決しません。このスキルでは、printToLog / printToString でセマンティクスツリーをダンプし、フレームワークに組み込まれた「マージされていないツリーで見つかりました」というヒントを読み取って、ファインダーの失敗を診断する方法をエージェントに教えます。

このスキルを使用するタイミング

  • テストが "Failed: assertExists. Reason: Expected exactly '1' node but could not find any node that satisfies: …" で失敗する場合。
  • テストが "Reason: Expected exactly '1' node but found 'N' nodes that satisfy: …" で失敗する場合。
  • エラーメッセージが "…were found in the unmerged tree. If you really wanted to match against merged tree, use useUnmergedTree = true." で終わり、開発者が対処方法に困っている場合。
  • 開発者がカスタムマッチャーを作成しており、SemanticsNode に直接アクセスする必要がある場合。
  • Thread.sleep を追加してテストを修正したものの、根本原因がタイミングの問題ではなく、間違ったタグである場合。このスキルのパターンに置き換えてください。skydoves directive #7 を参照してください。

このスキルを使用しないタイミング

  • 症状がタイミングに関係している場合(アニメーションが完了していない、アイドリングリソースが処理中など)。../../synchronization/synchronizing-with-idle/SKILL.md または ../../synchronization/testing-animations-deterministically/SKILL.md を使用してください。
  • テストがタグではなくテキストで検索しているためにファインダーが間違っている場合。../../finders/finding-nodes-by-tag-text-content/SKILL.md を使用して修正してください(skydoves directive #1)。
  • ノード検索ではなく、アクセシビリティ違反の診断に関する場合。../enabling-accessibility-checks/SKILL.md を使用してください。

前提条件

  • androidTestImplementation または testImplementation に androidx.compose.ui:ui-test が含まれていること(API は commonMain にあるため、どちらのソースセットでも利用できます)。
  • 有効な ComposeTestRule または ComposeUiTest があること(標準的な骨組み以外の追加設定は不要です)。

ワークフロー

  • 1. ファインダーが失敗したら、まずマージ済みツリーを出力します。 rule.setContent { … } の直後(または失敗するファインダーの直前)に行を追加します。
rule.onRoot().printToLog("DEBUG")

シグネチャは fun SemanticsNodeInteraction.printToLog(tag: String, maxDepth: Int = Int.MAX_VALUE) です。単一ノードのレシーバーでは、デフォルトでサブツリー全体を出力します。参照先は compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Output.kt:62-69 です。出力は adb logcat -s DEBUG で確認できます。

  • 2. マージ済みダンプに想定したノードが表示されない場合は、未マージツリーを出力します。
rule.onRoot(useUnmergedTree = true).printToLog("DEBUG")

マージを行うコンポーザブル(例:Button 内の Icon)の内部の子孫ノードは、未マージツリーにのみ表示されます。

  • 3. 出力の文法を確認します。 形式は Output.kt:135-296 で固定されています。
Node #<id> at (l=<L>, t=<T>, r=<R>, b=<B>)px, Tag: '<tag>'
  <Sorted ConfigKey> = '<value>'
  ...
  [<FlagKey1>, <FlagKey2>]            // keys whose value is Unit
  Actions = [<ActionKey1>, ...]        // AccessibilityActions and Function values
  MergeDescendants = 'true'            // when isMergingSemanticsOfDescendants
  ClearAndSetSemantics = 'true'        // when isClearingSemantics
   |-Node #<childId> at (...)px        // children indented with " |-"
  • 先頭行:Node # + ID + 境界 +(任意で)Tag: '<testTag>'。

  • 設定項目はキー名のアルファベット順に並べ替えられます。

  • AccessibilityAction と Function<*> の値は Actions = [keyName, …] として要約されます。

  • Unit の値(例:Disabled、IsTraversalGroup)は [Disabled, IsTraversalGroup, …] として要約されます。

  • MergeDescendants = 'true' と ClearAndSetSemantics = 'true' のフラグは、ノードのブロックの末尾に表示されます。

  • 4. フレームワーク組み込みのヒントを確認します。 onNode(...) がマージ済みツリーで一致するノードを見つけられず、同じマッチャーが未マージツリーでは一致する場合、エラーメッセージに「These nodes were found in the unmerged tree」というブロックが含まれます。参照先は compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction.kt:184-194 です。この表示があった場合は、失敗したファインダーで useUnmergedTree = true に切り替えます。マージ済み/未マージの判断については、../../finders/finding-nodes-by-tag-text-content/SKILL.md を参照してください。

  • 5. カスタムマッチャーでは、fetchSemanticsNode / fetchSemanticsNodes を直接使用します。 これらは、直接調査できるように、基になる SemanticsNode(複数可)を返します。

val node = rule.onNodeWithTag("title").fetchSemanticsNode()
assertThat(node.config[SemanticsProperties.Text]).isNotEmpty()

val all = rule.onAllNodesWithTag("row").fetchSemanticsNodes(
    atLeastOneRootRequired = true,
    errorMessageOnFail = "expected at least one row",
)

単一ノードのシグネチャ:fun fetchSemanticsNode(errorMessageOnFail: String? = null): SemanticsNode。コレクションのシグネチャ:fun fetchSemanticsNodes(atLeastOneRootRequired: Boolean = true, errorMessageOnFail: String? = null): List<SemanticsNode>。

  • 6. コレクションファインダーのデフォルトの maxDepth は 0(子なし)です。 各一致結果のサブツリー全体を確認したい場合は、値を変更します。
rule.onAllNodesWithTag("row").printToLog("DEBUG", maxDepth = Int.MAX_VALUE)

参照先は Output.kt:83-116 です。

  • 7. アサーションベースのデバッグ用に、ダンプを String として取得します。 CI ログで役立ちます。
val tree = rule.onRoot(useUnmergedTree = true).printToString()
println(tree) // or attach to a test report

パターン

パターン:ツリーを出力する代わりに、ノードを「待つ」ために Thread.sleep を使う
// WRONG
@Test
fun appears() {
    rule.setContent { MyScreen() }
    Thread.sleep(2000)                                    // hide the symptom
    rule.onNodeWithTag("submit").assertIsDisplayed()      // still fails
}
// WRONG because: Thread.sleep desyncs from MainTestClock and does not advance composition.
// If the node is missing, more wall time will not produce it. The cause is structural.
// Skydoves directive #7: Thread.sleep is a smell.
// RIGHT
@Test
fun appears() {
    rule.setContent { MyScreen() }
    rule.onRoot(useUnmergedTree = true).printToLog("DEBUG")   // see what's actually present
    rule.onNodeWithTag("submit").assertIsDisplayed()
}
パターン:「未マージツリーで見つかりました」というヒント
// WRONG — the test ignores the framework hint
@Test
fun iconExists() {
    rule.setContent { Button(onClick = {}) { Icon(Icons.Default.Add, modifier = Modifier.testTag("add-icon")) } }
    rule.onNodeWithTag("add-icon").assertExists()
    // AssertionError: ... 0 matches.
    // These nodes were found in the unmerged tree:
    //   Node #4 ... Tag: 'add-icon'
    // If you really wanted to match against merged tree, use useUnmergedTree = true.
}
// WRONG because: a Button merges descendants. The Icon's tag is collapsed into the Button
// node in the merged tree. The error message names the fix in plain English.
// RIGHT
@Test
fun iconExists() {
    rule.setContent { Button(onClick = {}) { Icon(Icons.Default.Add, modifier = Modifier.testTag("add-icon")) } }
    rule.onNodeWithTag("add-icon", useUnmergedTree = true).assertExists()
}
パターン:「3 個のノードが見つかりました」という曖昧な結果 — ダンプで特定する
// WRONG
rule.onNodeWithTag("row").assertIsDisplayed()
// AssertionError: Reason: Expected exactly '1' node but found '3' nodes that satisfy:
// (TestTag = 'row')
// RIGHT — dump first, then narrow the matcher
rule.onAllNodesWithTag("row").printToLog("DEBUG", maxDepth = Int.MAX_VALUE)
// Read the dump to find a stable disambiguator (e.g. parent tag, contentDescription),
// then narrow:
rule.onAllNodesWithTag("row")
    .filterToOne(hasAnyAncestor(hasTestTag("section-completed")))
    .assertIsDisplayed()
パターン:カスタムアサーションで fetchSemanticsNode を使う
// RIGHT
@Test
fun rowConfigContainsCustomKey() {
    rule.setContent { /* … */ }
    val node = rule.onNodeWithTag("row").fetchSemanticsNode("missing 'row'")
    val custom: String? = node.config.getOrNull(MyCustomKey)
    assertThat(custom).isEqualTo("expected")
}

必須ルール

  • 必須: ファインダーが失敗したら、ただちに rule.onRoot().printToLog("DEBUG")(または未マージ版)でセマンティクスツリーをダンプします。単一ノードのレシーバーにおける maxDepth のデフォルト値は Int.MAX_VALUE なので、追加設定なしでサブツリー全体が出力されます。参照:Output.kt:43, 65。
  • 必須: フレームワークのヒント "…were found in the unmerged tree. If you really wanted to match against merged tree, use useUnmergedTree = true." を指示として読み、失敗したファインダーで useUnmergedTree = true に切り替えます。参照:SemanticsNodeInteraction.kt:184-194。
  • 禁止: 「ツリーが読み込まれるまで待つ」ために Thread.sleep(…) を追加しないでください。ファインダーの失敗は構造上の問題であり、待ち時間を増やしてもノードは出現しません。Skydoves の指示 #7。唯一正当な用途は、スクリーンショットや RenderThread の待機です。
  • 必須: SemanticsNodeInteractionCollection に対して printToLog を呼び出す際は、maxDepth をゼロ以外の値に設定します。コレクションのデフォルト値は 0(子ノードなし)です。参照:Output.kt:85, 112。
  • 推奨: マージ済みツリーから診断を始め、予期したノードが折りたたまれている場合に限り、useUnmergedTree = true に切り替えます。Skydoves の指示 #2:デフォルトでは useUnmergedTree = false を維持します。
  • 推奨: カスタムマッチャーでは、失敗メッセージにアサーションの行だけでなくテストを特定させるため、fetchSemanticsNode(errorMessageOnFail = "…") を優先します。

検証

  • ファインダーが失敗した場合、テスト(またはエージェントによる対話型デバッグ)で、該当するサブツリーの printToLog("DEBUG") 行が少なくとも 1 行出力されます。
  • ファインダーの問題を解決するために Thread.sleep が追加されていません。
  • ダンプでノードが useUnmergedTree = true の場合にのみ表示されるなら、ファインダーがそのフラグを渡すように更新されています。
  • 複数の一致が見つかった場合、sleep やアサーションの試行回数を増やすのではなく、filterToOne、hasAnyAncestor、親の testTag などを使ってファインダーを絞り込んでいます。
  • カスタムマッチャーが、意味のある診断メッセージを指定した fetchSemanticsNode(errorMessageOnFail = "…") を使用しています。
  • CI では、テストログにダンプ出力が含まれています(開発者がテストプロセスの logcat を収集します)。

参考資料

コメント