compose-tests
stripe/stripe-android/.agents/skills/compose-tests/SKILL.md
Use when writing Compose UI tests in stripe-android — covers composeRule setup, Robolectric annotations, node assertions, and test tag patterns
Skill1.5k starsChanged 7 days ago
---
name: compose-tests
description: Use when writing Compose UI tests in stripe-android — covers composeRule setup, Robolectric annotations, node assertions, and test tag patterns
---
# 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:
```kotlin
@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`:
```kotlin
@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()`:
```kotlin
// 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
```kotlin
// Wait for pending recompositions and then for async content
composeRule.waitUntilWithIdle(
conditionDescription = "node with tag $MY_TAG to appear",
) {
composeRule.onAllNodesWithTag(MY_TAG)
.fetchSemanticsNodes(atLeastOneRootRequired = false)
.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
- **Calling `waitUntil` directly for async UI** — use `waitUntilWithIdle` for the standard idle
synchronization and 5,000 ms timeout
- **Requiring a Compose root while polling** — pass `atLeastOneRootRequired = false` to
`fetchSemanticsNodes`
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

