Composables

Asserting Node State And Text

Community

skydoves

Use this skill to verify a Compose semantics node's properties from a UI test using `assertExists`, `assertDoesNotExist`, `assertIsDisplayed`, `assertIsNotDisplayed`, `assertIsDeactivated`, `assertIsEnabled`, `assertIsOn`, `assertIsOff`, `assertIsSelected`, `assertIsFocused`, `assertTextEquals`, `assertTextContains`, `assertContentDescriptionEquals`, `assertValueEquals`, `assertRangeInfoEquals`, `assertHasClickAction`, plus the generic `assert(matcher)` escape hatch and the boolean `isDisplayed()` / `isNotDisplayed()` for `waitUntil` predicates. Covers collection variants `assertCountEquals`, `assertAny`, `assertAll`. Use when the developer wants to verify a Switch is on, a Button is enabled, a Text shows the expected string, a node is displayed vs merely composed, or asks about `assertIsDisplayed` vs `assertExists`. If the developer mentions any `assert*` API on `SemanticsNodeInteraction`, use this skill.

Overview

Repository
skydoves/android-testing-skills
Publisher
skydoves
Stars
347
Forks
14
Bundled files
0
License
Apache-2.0
Updated
May 25, 2026

Installation

tmp=$(mktemp -d)
git clone --depth 1 https://github.com/skydoves/android-testing-skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/compose/assertions/asserting-node-state-and-text" ~/.claude/skills/asserting-node-state-and-text
rm -rf "$tmp"

Instructions

Asserting Node State and Text — Verify Through the Framework, Not Through fetchSemanticsNode

Once a finder resolves to a SemanticsNodeInteraction, the next step is asserting it. Compose ships a typed assertion for almost every semantic property; the generic assert(matcher) covers the rest. This skill picks the right assertion, distinguishes "exists" from "displayed", and shows when boolean predicates belong inside waitUntil.

When to use this skill

  • The developer wants to verify a Switch is on, a Checkbox is unchecked, a Button is enabled, a Text equals an expected string.
  • The developer asks about assertIsDisplayed vs assertExists (one verifies on-screen, the other verifies presence in the tree).
  • The developer needs a custom assertion via assert(matcher) for a property without a typed extension.
  • The developer wants to verify a collection: "exactly 3 items", "at least one is selected", "all are enabled".
  • A test compares node.config[...] directly instead of using a typed assertion.

When NOT to use this skill

  • The right node cannot be located — see ../../finders/finding-nodes-by-tag-text-content/SKILL.md.
  • The assertion is geometric (width, height, position) — see ./asserting-bounds-and-dimensions/SKILL.md.
  • The check needs a custom SemanticsMatcher — see ../../finders/composing-semantics-matchers/SKILL.md.
  • The state changes asynchronously and the assertion is timing-sensitive — see ../../synchronization/synchronizing-with-idle/SKILL.md.

Prerequisites

  • A working ComposeTestRule / ComposeUiTest. See ../../setup/configuring-test-dependencies/SKILL.md.
  • The node has been located through onNode* / onAllNodes* / a navigator.
  • For assertIsDisplayed semantics, the node must be composed AND placed AND at least partially on-screen post-clip (Assertions.kt:30-39).

Workflow

  • 1. Pick by question type. Map the developer's question to one assertion call. Every API listed is in commonMain/.../Assertions.kt unless noted.

    Question API File:line
    Is it in the tree? assertExists(errorMessageOnFail) SemanticsNodeInteraction.kt:124-127
    Is it gone? assertDoesNotExist() SemanticsNodeInteraction.kt:94-110
    Is it a SubcomposeLayout retained child? assertIsDeactivated() SemanticsNodeInteraction.kt:137-148
    Is it visible to the user? assertIsDisplayed() / assertIsNotDisplayed() Assertions.kt:32-53
    Is it interactive? assertIsEnabled() / assertIsNotEnabled() Assertions.kt:60-67
    Is the toggle on? assertIsOn() / assertIsOff() / assertIsToggleable() Assertions.kt:74-81, 103
    Is the radio/tab selected? assertIsSelected() / assertIsNotSelected() / assertIsSelectable() Assertions.kt:88-110
    Does it have focus? assertIsFocused() / assertIsNotFocused() Assertions.kt:117-124
    Does it equal these strings? assertTextEquals(vararg, includeEditableText=true) Assertions.kt:181-185
    Does it contain this string? assertTextContains(value, substring=false, ignoreCase=false) Assertions.kt:204-208
    Content description equals these? assertContentDescriptionEquals(vararg) Assertions.kt:139-141
    Content description contains this? assertContentDescriptionContains(value, substring, ignoreCase) Assertions.kt:158-163
    StateDescription equals X? assertValueEquals(value) Assertions.kt:216-217
    ProgressBar at this position? assertRangeInfoEquals(rangeInfo) Assertions.kt:225-227
    Has a click action? assertHasClickAction() / assertHasNoClickAction() Assertions.kt:234-243
    Anything else assert(matcher, messagePrefixOnError = null) Assertions.kt:254-267
  • 2. assertIsDisplayed vs assertExists — pick deliberately. A node assertExists() if it is in the semantics tree (composed). A node assertIsDisplayed() if it is composed AND placed AND at least partially visible after clipping (Assertions.kt:30-39). For LazyColumn items not yet scrolled into view, assertExists() may fail (item not composed) or succeed (item composed but off-screen — depends on prefetch); assertIsDisplayed() is the right gate for "the user sees this".

  • 3. Use the typed assertion, not fetchSemanticsNode().config[...]. Typed assertions delegate to the matcher library and produce framework-formatted errors that name the failed property and dump the node (Assertions.kt:254-267 — the generic assert builds buildGeneralErrorMessage(errorMessageOnFail, selector, node)). Direct config reads bypass that error reporting and miss the auto-attached node dump.

  • 4. For collections, use the collection-typed assertions. assertCountEquals(expectedSize) for cardinality (Assertions.kt:276-292), assertAny(matcher) for "at least one matches" (fails on empty — Assertions.kt:300-312), assertAll(matcher) for "every node matches" (passes on empty — Assertions.kt:323-339).

  • 5. For waitUntil predicates, use boolean APIs, not throwing assertions. isDisplayed() returns Boolean (Assertions.kt:351) — perfect for rule.waitUntil { node.isDisplayed() }. isNotDisplayed() is its inverse (Assertions.kt:361). Both throw if multiple nodes match the finder, but never on zero. Cross-reference: ../../synchronization/synchronizing-with-idle/SKILL.md.

  • 6. For one-off properties without a typed assertion, use assert(matcher). Combine with prebuilt or composed matchers from ../../finders/composing-semantics-matchers/SKILL.md. The messagePrefixOnError lambda is for adding context when this assert is a precondition for a larger operation.

Patterns

Pattern: assertIsDisplayed over assertExists for user-facing checks
// WRONG
@Test
fun submit_isVisibleAfterError() {
    rule.setContent { CheckoutScreen(state = state) }
    rule.onNodeWithTag(SubmitTag).assertExists()
}
// WRONG because: assertExists passes for nodes that are composed but off-screen, behind a
// dialog, or measured to zero size. The test does not prove the user actually sees the button.
// RIGHT
@Test
fun submit_isVisibleAfterError() {
    rule.setContent { CheckoutScreen(state = state) }
    rule.onNodeWithTag(SubmitTag).assertIsDisplayed()
}
Pattern: typed assertIsOn over manual config reads
// WRONG
@Test
fun darkMode_switch_isOn() {
    rule.setContent { SettingsScreen() }
    val node = rule.onNodeWithTag(DarkModeSwitchTag).fetchSemanticsNode()
    assertEquals(ToggleableState.On, node.config[SemanticsProperties.ToggleableState])
}
// WRONG because: bypasses the framework's error reporting. Failure prints a JUnit assertEquals
// diff with no node dump, no selector description, no hint about which screen the failure
// happened on.
// RIGHT
@Test
fun darkMode_switch_isOn() {
    rule.setContent { SettingsScreen() }
    rule.onNodeWithTag(DarkModeSwitchTag).assertIsOn()
}

assertIsOn is assert(isOn()) (Assertions.kt:74); isOn() is expectValue(SemanticsProperties.ToggleableState, ToggleableState.On) (Filters.kt:61-62). The error names the property and dumps the node automatically.

Pattern: assertTextEquals (vararg) handles merged Text + EditableText
// production:
TextField(value = "hello", onValueChange = {}, label = { Text("Name") },
    modifier = Modifier.testTag(NameFieldTag))

// test:
rule.onNodeWithTag(NameFieldTag).assertTextEquals("Name", "hello")

assertTextEquals(vararg) matches the unordered set of SemanticsProperties.Text plus, by default, SemanticsProperties.EditableText (Assertions.kt:181-185 → Filters.kt:274-293). To exclude editable text from the comparison: assertTextEquals("Name", includeEditableText = false).

Pattern: assertCountEquals instead of onAllNodes(...).onFirst().assertExists()
// WRONG
rule.onAllNodesWithTag(ItemTag).onFirst().assertExists()
// WRONG because: only proves there is at least one item. Drift to zero items causes
// "no node matched" instead of a count error; drift to many is silently accepted.
// RIGHT
rule.onAllNodesWithTag(ItemTag).assertCountEquals(3)
Pattern: assert(matcher) for properties without a typed assertion
@Test
fun row_hasPriorityOne() {
    rule.setContent { TaskList(tasks = tasks) }

    rule.onNodeWithTag(TaskRowTag)
        .assert(SemanticsMatcher.expectValue(PriorityKey, 1))
}

PriorityKey is a custom SemanticsPropertyKey<Int> set via Modifier.semantics { priority = … }. See ../../finders/composing-semantics-matchers/SKILL.md for matcher composition.

Pattern: assertAny (fails on empty) vs assertAll (passes on empty)
// "at least one row is selected" — must have at least one row
rule.onAllNodesWithTag(RowTag).assertAny(isSelected())

// "every row is enabled" — accepts zero rows (passes vacuously)
rule.onAllNodesWithTag(RowTag).assertAll(isEnabled())

assertAny throws AssertionError("Failed to assertAny … no node matched") on an empty collection (Assertions.kt:305-307). assertAll returns successfully on an empty collection (Assertions.kt:323-339). Pick deliberately.

Pattern: isDisplayed() inside waitUntil
// WRONG
rule.waitUntil { rule.onNodeWithTag(SnackbarTag).assertIsDisplayed(); true }
// WRONG because: assertIsDisplayed throws on the first poll where the snackbar isn't yet
// shown. waitUntil expects a boolean — predicates that throw will fail-fast.
// RIGHT
rule.waitUntil(timeoutMillis = 2_000) {
    rule.onAllNodesWithTag(SnackbarTag).fetchSemanticsNodes().isNotEmpty() &&
        rule.onNodeWithTag(SnackbarTag).isDisplayed()
}
rule.onNodeWithTag(SnackbarTag).assertTextContains("Saved")

isDisplayed() returns false when zero nodes match and only throws on multiple matches (Assertions.kt:343-351). Cross-reference: ../../synchronization/synchronizing-with-idle/SKILL.md for waitUntil vs mainClock.advanceTimeUntil.

Pattern: assertIsDeactivated for SubcomposeLayout retained children
// LookaheadLayout / SubcomposeLayout may retain a previously composed slot for reuse.
// To verify the slot is currently deactivated (kept, not active):
rule.onNodeWithTag(RetainedSlotTag).assertIsDeactivated()

assertIsDeactivated fetches the node without skipping deactivated ones (SemanticsNodeInteraction.kt:137-148) and checks node.layoutInfo.isDeactivated.

Mandatory rules

  • MUST use the typed assertion (assertIsOn, assertIsEnabled, assertTextEquals, …) over fetchSemanticsNode().config[...]. The framework's error message includes the selector, the node dump, and the failed clause.
  • MUST use assertIsDisplayed() when the contract is "the user sees this"; MUST use assertExists() only when the contract is "this is in the semantics tree" (e.g. checking presence in unmerged tree without on-screen requirement).
  • MUST use the boolean isDisplayed() / isNotDisplayed() inside waitUntil { … } blocks; MUST NOT call throwing assertIs* from inside waitUntil.
  • MUST use assertCountEquals for collection cardinality; MUST NOT index [0] to imply "the only one".
  • MUST NOT repeat the same assertion across chained calls when one composed matcher would do — see ../../finders/composing-semantics-matchers/SKILL.md.
  • PREFERRED: prefer tag-anchored lookups before assertions. Skydoves hot take #1.

Verification

  • No assertion reads node.config[...] directly. All property checks go through assertIs* / assertTextEquals / assert(matcher).
  • assertIsDisplayed is used for user-visible checks; assertExists only when on-screen visibility is irrelevant.
  • waitUntil { … } blocks use isDisplayed() / isNotDisplayed() / fetchSemanticsNodes().size — no throwing assertIs* calls inside.
  • assertAny / assertAll choice matches the intended empty-collection semantics.
  • ./gradlew :app:connectedDebugAndroidTest (device) or :app:testDebugUnitTest (host) passes.
  • Failure messages mention the offending semantic property by name (proof the typed assertion is in use).

References

FAQ

What does Asserting Node State And Text do?

Use this skill to verify a Compose semantics node's properties from a UI test using `assertExists`, `assertDoesNotExist`, `assertIsDisplayed`, `assertIsNotDisplayed`, `assertIsDeactivated`, `assertIsEnabled`, `assertIsOn`, `assertIsOff`, `assertIsSelected`, `assertIsFocused`, `assertTextEquals`, `assertTextContains`, `assertContentDescriptionEquals`, `assertValueEquals`, `assertRangeInfoEquals`, `assertHasClickAction`, plus the generic `assert(matcher)` escape hatch and the boolean `isDisplayed()` / `isNotDisplayed()` for `waitUntil` predicates. Covers collection variants `assertCountEquals`, `assertAny`, `assertAll`. Use when the developer wants to verify a Switch is on, a Button is enabled, a Text shows the expected string, a node is displayed vs merely composed, or asks about `assertIsDisplayed` vs `assertExists`. If the developer mentions any `assert*` API on `SemanticsNodeInteraction`, use this skill.

Who made Asserting Node State And Text?

skydoves (a community member) publishes it in the skydoves/android-testing-skills repository on GitHub.

Is Asserting Node State And Text free?

Yes. Asserting Node State And Text is open source, published by skydoves under the Apache-2.0 license.

What comes with Asserting Node State And Text?

Just its instructions. Everything the agent needs is in one SKILL.md file.

Which agents can use Asserting Node State And Text?

Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.

What are agent skills?

An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.

How are agent skills different from MCP servers?

An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.

How can my agent test my Android app?

Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.

You're subscribed

The next lesson lands in on Wednesday.