compose-tests
$
npx mdskill add stripe/stripe-android/compose-testsGenerate Stripe Android Compose UI test code with correct annotations.
- Creates test classes with Robolectric runners and required Compose rules.
- Integrates with Stripe Android testing libraries for resource management.
- Executes test scenarios using runScenario to verify UI state changes.
- Outputs Kotlin code ready for integration into the Stripe SDK.
SKILL.md
.github/skills/compose-testsView on GitHub ↗
---
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
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
More from stripe/stripe-android
- create-fakeUse when creating a fake test implementation in stripe-android — covers FakeClassName pattern, Turbine call tracking, ViewActionRecorder, and ensureAllEventsConsumed validation
- network-testsUse when writing NetworkRule integration tests in stripe-android — covers testBodyFromFile, inline JSON modification, request matchers, and fixture patterns
- screenshot-testsUse when writing or running Paparazzi screenshot tests in stripe-android — covers PaparazziRule setup, recording/verifying commands, and test structure
- write-unit-testsUse when writing or structuring unit tests in stripe-android — covers runScenario pattern, fakes, Turbine Flow testing, and Truth assertions