mcpbeat

Compose Tests

stripe/compose-tests

Use when writing Compose UI tests in stripe-android — covers composeRule setup, Robolectric annotations, node assertions, and test tag patterns

981 tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
1532
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/stripe/stripe-android --skill compose-tests

The instruction itself

9 sections, as written by the author

Compose UI Tests

Patterns for testing Jetpack Compose UI in the Stripe Android SDK.

Test Class Setup

Every Compose test needs three rules and Robolectric:

@RunWith(RobolectricTestRunner::class)
internal class MyComposableTest {
    @get:Rule
    val composeRule = createComposeRule()

    @get:Rule
    val composeCleanupRule = createComposeCleanupRule()

    @get:Rule
    val coroutineTestRule = CoroutineTestRule(UnconfinedTestDispatcher())
}
  • createComposeRule() — provides the Compose test harness
  • createComposeCleanupRule() — workaround for Robolectric resource leak (import from com.stripe.android.testing)
  • CoroutineTestRule — controls coroutine dispatching in tests

runScenario for Compose Tests

Use the same runScenario pattern from write-unit-tests, wrapping composeRule.setContent:

@Test
fun `required fields are visible`() = runScenario {
    page.country.assertIsDisplayed()
    page.zipCode.assertIsDisplayed()
    page.line1.assertDoesNotExist()
}

@Test
fun `input changes update state`() = runScenario(
    addressCollectionMode = AddressCollectionMode.Full,
) {
    page.zipCode.performTextReplacement("94100")
    assertThat(formState?.postalCode?.value).isEqualTo("94100")
}

private fun runScenario(
    addressCollectionMode: AddressCollectionMode = AddressCollectionMode.Automatic,
    block: TestScenario.() -> Unit,
) {
    composeRule.setContent {
        MyComposableUI(addressCollectionMode = addressCollectionMode)
    }
    block(TestScenario(MyPage(composeRule)))
}

private data class TestScenario(val page: MyPage)

Finding Nodes

| Finder | Use when |

|--------|----------|

| onNodeWithTag(TAG) | Element has a test tag (preferred) |

| onNodeWithContentDescription("desc") | Testing accessibility only — prefer test tags otherwise |

| onAllNodesWithTag(TAG) | Multiple elements share a tag |

| onNode(hasTestTag(TAG).and(hasText("x"))) | Combining matchers |

Assertions

| Assertion | Purpose |

|-----------|---------|

| assertIsDisplayed() | Element is visible |

| assertExists() | Element exists (may not be visible) |

| assertDoesNotExist() | Element not in hierarchy |

| assertIsNotDisplayed() | Exists but not visible |

| assertIsEnabled() / assertIsNotEnabled() | Enabled state |

| assertTextContains("text") | Text field contains value |

| assertContentDescriptionContains("desc") | Accessibility description |

| assert(hasText("text")) | General matcher assertion |

Actions

| Action | Purpose |

|--------|---------|

| performClick() | Tap element |

| performTextReplacement("text") | Set text field value |

| performScrollTo() | Scroll element into view |

Test Tags

Define tags as constants in production code, use Modifier.testTag():

// In production code:
internal const val SAVE_BUTTON_TEST_TAG = "save_button"

Box(modifier = Modifier.testTag(SAVE_BUTTON_TEST_TAG)) { ... }

// In test code:
composeRule.onNodeWithTag(SAVE_BUTTON_TEST_TAG).assertIsDisplayed()

Waiting for Recomposition

// Wait for pending recompositions
composeRule.waitForIdle()

// Wait for async content with timeout
composeRule.waitUntil(timeoutMillis = DEFAULT_UI_TIMEOUT.inWholeMilliseconds) {
    composeRule.onAllNodesWithTag(MY_TAG).fetchSemanticsNodes().isNotEmpty()
}

Common Mistakes

  • Missing composeCleanupRule — causes resource leaks with Robolectric, flaky tests
  • Missing @RunWith(RobolectricTestRunner::class) — Compose tests need Android framework
  • Using onNodeWithText for dynamic content — prefer test tags for stability
  • Not calling waitForIdle() after state changes — assertions may run before recomposition

How to use it

Copy the folder

Take stripe/compose-tests from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.