mcpField
A
asserting-node-state-and-text

asserting-node-state-and-text

github.com/skydovesTesting & quality33964 installsUpdated 22w ago
Repository

About asserting-node-state-and-text

A Compose testing skill for developers asserting properties of located semantics nodes. It maps common checks to typed assertions and explains visibility, collection assertions, custom matchers, and boolean predicates for waitUntil.

Key features

  1. 01Maps node properties to typed assertions
  2. 02Distinguishes assertExists from assertIsDisplayed
  3. 03Covers collection count, any, and all checks
  4. 04Supports custom matcher assertions
  5. 05Uses boolean visibility checks with waitUntil

Use cases

  1. 01Checking a Compose toggle or text value
  2. 02Verifying a node is visible rather than merely present
  3. 03Asserting collection size or selection state

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/skydoves/android-testing-skills --skill asserting-node-state-and-text

After installing, type/asserting-node-state-and-textin Claude Code to confirm it loaded.

Details

From the project README

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.

    QuestionAPIFile: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 elseassert(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

Comments

Markdown supported