stripe/compose-tests
Use when writing Compose UI tests in stripe-android — covers composeRule setup, Robolectric annotations, node assertions, and test tag patterns
npx skills add https://github.com/stripe/stripe-android --skill compose-tests
Patterns for testing Jetpack Compose UI in the Stripe Android SDK.
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 harnesscreateComposeCleanupRule() — workaround for Robolectric resource leak (import from com.stripe.android.testing)CoroutineTestRule — controls coroutine dispatching in testsUse 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)
| 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 |
| 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 |
| Action | Purpose |
|--------|---------|
| performClick() | Tap element |
| performTextReplacement("text") | Set text field value |
| performScrollTo() | Scroll element into view |
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()
// 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()
}
composeCleanupRule — causes resource leaks with Robolectric, flaky tests@RunWith(RobolectricTestRunner::class) — Compose tests need Android frameworkonNodeWithText for dynamic content — prefer test tags for stabilitywaitForIdle() after state changes — assertions may run before recompositionTake stripe/compose-tests from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
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.