Wnb compose ui test
Skill wenubey/claude-android-skills/skills/wnb-compose-ui-test
Reusable Claude Code skills for Android — Kotlin, Jetpack Compose, unit + UI testing, animations
npx -y skills add wenubey/claude-android-skills --skill wnb-compose-ui-testAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 15 days oldThe repository was created 15 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill when writing, reviewing, or refactoring a Jetpack Compose UI test on Android. Enforces the canonical skeleton — @get:Rule createComposeRule(), an inline DispatcherProvider using Dispatchers.Unconfined so state propagates synchronously to the tree, a renderScreen(...) factory that constructs the ViewModel + sets content and returns the VM for assertion, semantics-first finders (onNodeWithText, onNodeWithTag, onNodeWithContentDescription, hasTestTag), Truth assertions on state, no Thread.sleep (waitForIdle / waitUntil instead), no hardcoded coordinates, no mocking of repositories (use fakes). Applies to instrumentation-style tests under app/src/androidTest. Triggers on "compose ui test", "createComposeRule", "androidTest", "instrumentation test", "onNodeWithText", "onNodeWithTag", "performClick", "performTextInput", "waitForIdle", "setContent", "compose test dispatcher", "test Screen composable".
SKILL.md
8.5 KB, as published. Nobody here has run it
Jetpack Compose UI test — canonical skeleton
Every screen-level Compose test in this project has the same shape. Same rule, same dispatcher trick, same factory pattern, same finder discipline. Do not invent variants.
Pairs with [[wnb-viewmodel-udf]] and [[wnb-viewmodel-test]] — a screen's ViewModel unit test verifies the state machine; this UI test verifies the state renders correctly and one critical interaction works.
Non-negotiables
@get:Rule val composeTestRule = createComposeRule()— one rule per test class. NevercreateAndroidComposeRule<Activity>()unless you specifically need the host Activity (rare).- Inline
DispatcherProviderwithDispatchers.Unconfinedfor all three (main,io,default). This makes VM state updates propagate to the tree synchronously — no missed emissions, noadvanceUntilIdle. Only exception: tests that specifically need to inspect intermediate loading state can use aStandardTestDispatcher+MainDispatcherRulecombo; the default is Unconfined. renderScreen(...)factory at the top of every test class. Constructs the VM with defaultable fake repositories, callscomposeTestRule.setContent { XxxScreen(viewModel = vm, …) }, and returns the VM so tests can assert againstvm.state.valuedirectly.- Semantics-first finders, in this preference order:
onNodeWithTag("submitButton")— best for interactive elements (addModifier.testTag(...)in the composable).onNodeWithContentDescription("Cart icon")— for icons/images; also earns you a11y coverage.onNodeWithText("Sign In")— for user-visible text. Fine for static labels; brittle for i18n strings and dynamic content.onNode(hasTestTag("...") and hasText("..."))— for compound matches.- Never coordinates (
onNodeAt(x, y)), never index-based sibling access.
- Assertion split. UI assertions on nodes:
assertIsDisplayed(),assertIsEnabled(),assertTextEquals(...),assertIsToggleable(). State assertions on the ViewModel via Truth:assertThat(vm.state.value.email).isEqualTo(...). composeTestRule.waitForIdle()after every interaction that triggers a coroutine or recomposition. NeverThread.sleep.- No mocking of repository interfaces. Use
TestXxxRepositoryfakes with in-memory state, mirroring the unit-test convention. - One test = one behavior. Render, one interaction (or none), one assertion cluster. If you need two flows, write two tests.
Canonical example
class FooScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
private val dispatcherProvider = object : DispatcherProvider {
override fun main(): CoroutineDispatcher = Dispatchers.Unconfined
override fun io(): CoroutineDispatcher = Dispatchers.Unconfined
override fun default(): CoroutineDispatcher = Dispatchers.Unconfined
}
private fun renderScreen(
repo: TestFooRepository = TestFooRepository(),
onNavigateToDetail: (String) -> Unit = {},
): FooViewModel {
val vm = FooViewModel(repo, dispatcherProvider)
composeTestRule.setContent {
FooScreen(
viewModel = vm,
onNavigateToDetail = onNavigateToDetail,
)
}
return vm
}
@Test
fun renders_title_and_empty_state_when_no_foos() {
renderScreen(repo = TestFooRepository(foos = emptyList()))
composeTestRule.onNodeWithText("Foos").assertIsDisplayed()
composeTestRule.onNodeWithText("No foos yet").assertIsDisplayed()
}
@Test
fun renders_list_of_foos_when_repo_returns_data() {
renderScreen(repo = TestFooRepository(foos = listOf(Foo("1", "Alpha"), Foo("2", "Beta"))))
composeTestRule.onNodeWithText("Alpha").assertIsDisplayed()
composeTestRule.onNodeWithText("Beta").assertIsDisplayed()
}
@Test
fun clicking_refresh_button_reloads_and_shows_new_items() {
val repo = TestFooRepository(foos = listOf(Foo("1", "Alpha")))
renderScreen(repo = repo)
composeTestRule.onNodeWithText("Alpha").assertIsDisplayed()
repo.setFoos(listOf(Foo("2", "Beta")))
composeTestRule.onNodeWithTag("refreshButton").performClick()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("Beta").assertIsDisplayed()
}
@Test
fun clicking_a_row_invokes_navigation_callback_with_id() {
var navigatedTo: String? = null
renderScreen(
repo = TestFooRepository(foos = listOf(Foo("42", "Alpha"))),
onNavigateToDetail = { navigatedTo = it },
)
composeTestRule.onNodeWithText("Alpha").performClick()
composeTestRule.waitForIdle()
assertThat(navigatedTo).isEqualTo("42")
}
@Test
fun error_state_shows_retry_and_dismisses_on_click() {
val vm = renderScreen(repo = TestFooRepository(refreshResult = Result.failure(RuntimeException("nope"))))
vm.onAction(FooAction.Refresh)
composeTestRule.waitForIdle()
composeTestRule.onNodeWithText("nope").assertIsDisplayed()
composeTestRule.onNodeWithTag("dismissErrorButton").performClick()
composeTestRule.waitForIdle()
assertThat(vm.state.value.error).isNull()
}
}
Common mistakes
Thread.sleep(500)to wait for recomposition → flaky on CI. UsecomposeTestRule.waitForIdle()orcomposeTestRule.waitUntil(2_000) { predicate }.- Using
Dispatchers.MaininDispatcherProvider→ hangs the test because there is no real main looper in the Compose test runner. UseDispatchers.Unconfined. onNodeWithText("Sign In")for a dynamic i18n string → breaks when copy changes. PreferonNodeWithTag("signInButton").- Hardcoded coordinates (
performTouchInput { down(Offset(200f, 400f)) }) → device-specific, brittle. Address by semantics. - Asserting only on the UI (
assertIsDisplayed) but never on VM state → misses regressions where the render is right but the state is wrong. Assert both when a behavior spans both. onNodeWithTexton merged text ("Email$errorSuffix") → won't match; text is merged from separateTextcomposables. UsehasText(..., substring = true)or a testTag.- Constructing the ViewModel in a
@Before/initblock → shared across tests, cross-test bleed. Always construct viarenderScreen(...)inside the test. - Mocking the repository interface → same reasoning as unit tests. Use a
TestFooRepositoryfake. setContent { }called twice in one test →IllegalStateException.renderScreenguarantees single call.- Reading
vm.state.valuebeforewaitForIdle()after an interaction → may read stale state. Always wait first. - Using
createAndroidComposeRule<MainActivity>()for a standalone screen → boots the whole app, slow, brittle. UsecreateComposeRule()unless the SUT explicitly reads from the hostActivity.
Preview screenshots vs UI tests
If the goal is "does this screen render for every state" — that's a screenshot / preview job (Paparazzi, Roborazzi, or the on-device HotSwan preview catalog), not a Compose UI test. Reserve Compose UI tests for behavior (state + interaction), not visual regression.
Related skills
[[wnb-viewmodel-udf]]— the ViewModel shape the SUT depends on.[[wnb-viewmodel-test]]— the unit-test companion. Split what each covers: unit test = state machine; UI test = renders + one critical interaction.[[wnb-compose-animation]]— animation authoring rules; testing animated UI has extra care aroundmainClock.advanceTimeBy.