android-sdd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,259 @@
1
+ ---
2
+ name: snapshot-testing
3
+ description: >
4
+ Snapshot (screenshot) testing for Android Compose UI.
5
+ Load this skill when implementing visual regression testing,
6
+ using Paparazzi for JVM-based screenshot tests, using Shot or
7
+ Roborazzi for device-based snapshots, or managing snapshot artifacts in CI.
8
+ ---
9
+
10
+ # Snapshot Testing
11
+
12
+ ## Overview
13
+ Snapshot testing captures a screenshot of a composable or screen and compares it against a stored reference image. If the UI changes unexpectedly, the test fails. This catches visual regressions that logic tests miss. Paparazzi runs on the JVM (no device needed); Roborazzi extends Robolectric for JVM-based Compose snapshots.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - Snapshot tests catch **visual regressions** — unexpected UI changes
20
+ - **JVM-based** (Paparazzi/Roborazzi) is preferred — fast, no emulator needed
21
+ - Test **components in isolation** — not full screens; focus on design system components
22
+ - Always test **multiple states** — loading, success, error, empty, dark mode, RTL
23
+ - **Commit snapshots** to version control — they are the source of truth
24
+ - **Update snapshots intentionally** — never auto-accept all failures
25
+
26
+ ---
27
+
28
+ ## Paparazzi (JVM-based, no device)
29
+
30
+ ```kotlin
31
+ // dependency: app.cash.paparazzi:paparazzi
32
+
33
+ // ✅ Basic Paparazzi test
34
+ class UserCardSnapshotTest {
35
+
36
+ @get:Rule
37
+ val paparazzi = Paparazzi(
38
+ deviceConfig = DeviceConfig.PIXEL_5,
39
+ theme = "android:Theme.Material.Light.NoActionBar"
40
+ )
41
+
42
+ @Test
43
+ fun userCard_success() {
44
+ paparazzi.snapshot {
45
+ AppTheme {
46
+ UserCard(
47
+ user = UserFactory.create(),
48
+ onUserClick = {}
49
+ )
50
+ }
51
+ }
52
+ }
53
+
54
+ @Test
55
+ fun userCard_longName() {
56
+ paparazzi.snapshot("long_name") {
57
+ AppTheme {
58
+ UserCard(
59
+ user = UserFactory.create(name = "Very Long Name That Might Overflow"),
60
+ onUserClick = {}
61
+ )
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // ✅ Test dark mode
68
+ class UserCardDarkModeTest {
69
+
70
+ @get:Rule
71
+ val paparazzi = Paparazzi(
72
+ deviceConfig = DeviceConfig.PIXEL_5.copy(
73
+ nightMode = NightMode.NIGHT
74
+ )
75
+ )
76
+
77
+ @Test
78
+ fun userCard_darkMode() {
79
+ paparazzi.snapshot {
80
+ AppTheme(darkTheme = true) {
81
+ UserCard(user = UserFactory.create(), onUserClick = {})
82
+ }
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Paparazzi Build Setup
91
+
92
+ ```kotlin
93
+ // build.gradle.kts
94
+ plugins {
95
+ alias(libs.plugins.paparazzi)
96
+ }
97
+
98
+ // libs.versions.toml
99
+ [versions]
100
+ paparazzi = "1.3.4"
101
+
102
+ [plugins]
103
+ paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Roborazzi (JVM-based with Robolectric)
109
+
110
+ ```kotlin
111
+ // dependency: io.github.takahirom.roborazzi:roborazzi
112
+ // dependency: io.github.takahirom.roborazzi:roborazzi-compose
113
+
114
+ // ✅ Roborazzi test
115
+ @RunWith(RobolectricTestRunner::class)
116
+ @Config(sdk = [34])
117
+ class UserCardRoborazziTest {
118
+
119
+ @get:Rule
120
+ val composeRule = createComposeRule()
121
+
122
+ @Test
123
+ fun userCard_defaultState() {
124
+ composeRule.setContent {
125
+ AppTheme {
126
+ UserCard(user = UserFactory.create(), onUserClick = {})
127
+ }
128
+ }
129
+
130
+ composeRule
131
+ .onNodeWithTag("user_card")
132
+ .captureRoboImage("snapshots/user_card_default.png")
133
+ }
134
+
135
+ @Test
136
+ fun userCard_allStates() {
137
+ val states = listOf(
138
+ "active" to UserFactory.create(),
139
+ "suspended" to UserFactory.suspended(),
140
+ "admin" to UserFactory.admin()
141
+ )
142
+
143
+ states.forEach { (name, user) ->
144
+ composeRule.setContent {
145
+ AppTheme {
146
+ UserCard(user = user, onUserClick = {})
147
+ }
148
+ }
149
+ composeRule
150
+ .onNodeWithTag("user_card")
151
+ .captureRoboImage("snapshots/user_card_$name.png")
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Parameterized Snapshot Tests
160
+
161
+ ```kotlin
162
+ // ✅ Test all states systematically
163
+ @RunWith(Parameterized::class)
164
+ class UserListSnapshotTest(
165
+ private val name: String,
166
+ private val state: UserListUiState
167
+ ) {
168
+ companion object {
169
+ @JvmStatic
170
+ @Parameterized.Parameters(name = "{0}")
171
+ fun states() = listOf(
172
+ arrayOf("loading", UserListUiState.Loading),
173
+ arrayOf("success", UserListUiState.Success(UserFactory.list())),
174
+ arrayOf("empty", UserListUiState.Success(emptyList())),
175
+ arrayOf("error", UserListUiState.Error("Something went wrong"))
176
+ )
177
+ }
178
+
179
+ @get:Rule
180
+ val paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_5)
181
+
182
+ @Test
183
+ fun userList_state() {
184
+ paparazzi.snapshot(name) {
185
+ AppTheme {
186
+ UserListContent(state = state, onUserClick = {})
187
+ }
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Running Snapshot Tests
196
+
197
+ ```bash
198
+ # ✅ Record new snapshots (first run or after intentional UI change)
199
+ ./gradlew recordPaparazziDebug
200
+
201
+ # ✅ Verify snapshots (CI)
202
+ ./gradlew verifyPaparazziDebug
203
+
204
+ # ✅ Roborazzi record
205
+ ./gradlew recordRoborazziDebug
206
+
207
+ # ✅ Roborazzi verify
208
+ ./gradlew verifyRoborazziDebug
209
+
210
+ # Failure output: diff image showing what changed
211
+ # Location: app/build/outputs/paparazzi/failures/
212
+ ```
213
+
214
+ ---
215
+
216
+ ## CI Setup
217
+
218
+ ```yaml
219
+ # .github/workflows/snapshot.yml
220
+ - name: Run snapshot tests
221
+ run: ./gradlew verifyPaparazziDebug
222
+
223
+ - name: Upload snapshot failures
224
+ if: failure()
225
+ uses: actions/upload-artifact@v3
226
+ with:
227
+ name: snapshot-failures
228
+ path: '**/build/outputs/paparazzi/failures/'
229
+ ```
230
+
231
+ ---
232
+
233
+ ## What to Snapshot vs What Not To
234
+
235
+ | Snapshot | Don't Snapshot |
236
+ |---|---|
237
+ | Design system components (buttons, cards, chips) | Full screens with complex state |
238
+ | All states of a component (loading/error/empty) | Components with real network data |
239
+ | Dark mode variants | Logic behavior (use unit tests) |
240
+ | RTL layout variants | Dynamic content (timestamps, counters) |
241
+ | Typography and spacing | Complex animated states |
242
+
243
+ ---
244
+
245
+ ## Anti-Patterns
246
+
247
+ - Snapshotting with dynamic data (timestamps, random IDs) — causes constant failures
248
+ - Never updating snapshots — defeats the purpose; stale snapshots mask real regressions
249
+ - Accepting all snapshot failures blindly — review each diff; only accept intentional changes
250
+ - Snapshotting full screens with nav — isolate components; full screen tests are fragile
251
+ - Not testing dark mode — dark mode regressions are common and invisible in light mode tests
252
+
253
+ ---
254
+
255
+ ## Related Skills
256
+ - `compose-testing` — Compose semantic tree tests (complementary to snapshots)
257
+ - `ui-testing` — behavior tests alongside visual tests
258
+ - `fake-data` — factories for consistent snapshot data
259
+ - `design-system` — the components that most benefit from snapshot testing
@@ -0,0 +1,293 @@
1
+ ---
2
+ name: ui-testing
3
+ description: >
4
+ UI testing for Android apps running on device or emulator.
5
+ Load this skill when writing instrumented UI tests, testing navigation
6
+ flows, verifying screen content under different states, or setting up
7
+ the UI test infrastructure with Hilt and test rules.
8
+ ---
9
+
10
+ # UI Testing
11
+
12
+ ## Overview
13
+ UI tests verify the full user experience by running on a real device or emulator. They interact with the UI through semantic actions and verify the rendered output. In Compose, the primary tool is `ComposeTestRule`. For View-based UI, Espresso is used. Both can be combined in the same test suite.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - UI tests are **slow** — run on CI only; prefer unit + integration tests for logic
20
+ - Use **semantic properties** for finding components — `contentDescription`, `testTag`
21
+ - **Fake the data layer** via Hilt — never hit real network in UI tests
22
+ - **One scenario per test** — a test that fails should point to exactly one thing
23
+ - Use **`StateFlow` seeding** to set up UI state before assertions
24
+
25
+ ---
26
+
27
+ ## Dependencies
28
+
29
+ ```kotlin
30
+ // build.gradle.kts
31
+ dependencies {
32
+ androidTestImplementation(libs.androidx.compose.ui.test.junit4)
33
+ androidTestImplementation(libs.hilt.android.testing)
34
+ kspAndroidTest(libs.hilt.compiler)
35
+ androidTestImplementation(libs.androidx.test.runner)
36
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Compose UI Test Basics
43
+
44
+ ```kotlin
45
+ // ✅ Basic Compose UI test
46
+ @HiltAndroidTest
47
+ class UserListScreenTest {
48
+
49
+ @get:Rule(order = 0)
50
+ val hiltRule = HiltAndroidRule(this)
51
+
52
+ @get:Rule(order = 1)
53
+ val composeRule = createAndroidComposeRule<MainActivity>()
54
+
55
+ @Inject lateinit var fakeRepository: FakeUserRepository
56
+
57
+ @Before
58
+ fun setup() { hiltRule.inject() }
59
+
60
+ @Test
61
+ fun showsLoadingInitially() {
62
+ composeRule.onNodeWithTag("loading_indicator").assertIsDisplayed()
63
+ }
64
+
65
+ @Test
66
+ fun showsUsersWhenLoaded() {
67
+ // Seed data
68
+ val users = listOf(User(id = "1", name = "Ali Rezaei", email = Email("ali@test.com")))
69
+ fakeRepository.emit(users)
70
+
71
+ composeRule
72
+ .onNodeWithText("Ali Rezaei")
73
+ .assertIsDisplayed()
74
+ }
75
+
76
+ @Test
77
+ fun showsEmptyStateWhenNoUsers() {
78
+ fakeRepository.emit(emptyList())
79
+
80
+ composeRule
81
+ .onNodeWithTag("empty_state")
82
+ .assertIsDisplayed()
83
+ }
84
+
85
+ @Test
86
+ fun navigatesToDetailOnUserClick() {
87
+ val users = listOf(User(id = "1", name = "Ali Rezaei", email = Email("ali@test.com")))
88
+ fakeRepository.emit(users)
89
+
90
+ composeRule
91
+ .onNodeWithText("Ali Rezaei")
92
+ .performClick()
93
+
94
+ composeRule
95
+ .onNodeWithTag("user_detail_screen")
96
+ .assertIsDisplayed()
97
+ }
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Test Tags
104
+
105
+ ```kotlin
106
+ // ✅ Add testTag to composables for reliable test targeting
107
+ @Composable
108
+ fun LoadingIndicator(modifier: Modifier = Modifier) {
109
+ CircularProgressIndicator(
110
+ modifier = modifier.testTag("loading_indicator")
111
+ )
112
+ }
113
+
114
+ @Composable
115
+ fun UserListContent(
116
+ state: UserListUiState,
117
+ onUserClick: (String) -> Unit
118
+ ) {
119
+ when (state) {
120
+ is UserListUiState.Loading -> LoadingIndicator()
121
+ is UserListUiState.Success -> LazyColumn(
122
+ modifier = Modifier.testTag("user_list")
123
+ ) {
124
+ items(state.users, key = { it.id }) { user ->
125
+ UserRow(
126
+ user = user,
127
+ onClick = { onUserClick(user.id) },
128
+ modifier = Modifier.testTag("user_item_${user.id}")
129
+ )
130
+ }
131
+ }
132
+ is UserListUiState.Error -> ErrorView(
133
+ message = state.message,
134
+ modifier = Modifier.testTag("error_view")
135
+ )
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Testing Isolated Composables (No Activity)
143
+
144
+ ```kotlin
145
+ // ✅ Test a composable in isolation — no Activity, no navigation
146
+ class UserCardTest {
147
+
148
+ @get:Rule
149
+ val composeRule = createComposeRule()
150
+
151
+ @Test
152
+ fun displaysUserName() {
153
+ val user = User(id = "1", name = "Ali Rezaei", email = Email("ali@test.com"))
154
+ var clicked = false
155
+
156
+ composeRule.setContent {
157
+ AppTheme {
158
+ UserCard(user = user, onUserClick = { clicked = true })
159
+ }
160
+ }
161
+
162
+ composeRule.onNodeWithText("Ali Rezaei").assertIsDisplayed()
163
+ composeRule.onNodeWithText("ali@test.com").assertIsDisplayed()
164
+ }
165
+
166
+ @Test
167
+ fun invokesCallbackOnClick() {
168
+ val user = User(id = "1", name = "Ali Rezaei", email = Email("ali@test.com"))
169
+ var clickedId: String? = null
170
+
171
+ composeRule.setContent {
172
+ AppTheme {
173
+ UserCard(user = user, onUserClick = { clickedId = it })
174
+ }
175
+ }
176
+
177
+ composeRule.onNodeWithText("Ali Rezaei").performClick()
178
+ assertThat(clickedId).isEqualTo("1")
179
+ }
180
+ }
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Common Node Finders
186
+
187
+ ```kotlin
188
+ // By text
189
+ composeRule.onNodeWithText("Submit")
190
+ composeRule.onNodeWithText("Submit", ignoreCase = true)
191
+
192
+ // By testTag
193
+ composeRule.onNodeWithTag("loading_indicator")
194
+
195
+ // By contentDescription (accessibility)
196
+ composeRule.onNodeWithContentDescription("Back")
197
+
198
+ // By semantic role
199
+ composeRule.onNode(hasClickAction())
200
+ composeRule.onNode(isDialog())
201
+
202
+ // By position in list
203
+ composeRule.onAllNodesWithTag("user_item").onFirst()
204
+ composeRule.onAllNodesWithTag("user_item")[2]
205
+
206
+ // Combined matchers
207
+ composeRule.onNode(hasText("Ali") and hasTestTag("user_item_1"))
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Common Actions and Assertions
213
+
214
+ ```kotlin
215
+ // Actions
216
+ node.performClick()
217
+ node.performTextInput("test input")
218
+ node.performTextClearance()
219
+ node.performScrollTo()
220
+ node.performScrollToIndex(5)
221
+
222
+ // Assertions
223
+ node.assertIsDisplayed()
224
+ node.assertIsNotDisplayed()
225
+ node.assertIsEnabled()
226
+ node.assertIsNotEnabled()
227
+ node.assertTextEquals("Expected")
228
+ node.assertTextContains("partial")
229
+ node.assertContentDescriptionEquals("desc")
230
+ node.assertExists()
231
+ node.assertDoesNotExist()
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Waiting for Async Operations
237
+
238
+ ```kotlin
239
+ // ✅ Compose test rule waits for recomposition automatically
240
+ // For async operations that need explicit waiting:
241
+ composeRule.waitUntil(timeoutMillis = 5_000) {
242
+ composeRule.onAllNodesWithTag("user_item").fetchSemanticsNodes().isNotEmpty()
243
+ }
244
+
245
+ // ✅ Advance time in tests with test dispatcher
246
+ composeRule.mainClock.autoAdvance = false
247
+ composeRule.mainClock.advanceTimeBy(1_000)
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Hilt Test Module
253
+
254
+ ```kotlin
255
+ // ✅ Replace data layer with fakes for UI tests
256
+ @Module
257
+ @TestInstallIn(
258
+ components = [SingletonComponent::class],
259
+ replaces = [UserRepositoryModule::class]
260
+ )
261
+ abstract class FakeUserRepositoryModule {
262
+ @Binds @Singleton
263
+ abstract fun bindUserRepository(fake: FakeUserRepository): UserRepository
264
+ }
265
+
266
+ // ✅ FakeUserRepository is injectable in tests
267
+ class FakeUserRepository @Inject constructor() : UserRepository {
268
+ private val _users = MutableStateFlow<List<User>>(emptyList())
269
+
270
+ override fun observeUsers(): Flow<List<User>> = _users
271
+
272
+ fun emit(users: List<User>) { _users.value = users }
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Anti-Patterns
279
+
280
+ - Finding nodes by raw text that changes with localization — prefer `testTag`
281
+ - Testing implementation details like ViewModel state directly — test visible UI behavior
282
+ - Real network calls in UI tests — always fake the data layer
283
+ - Not wrapping composables in `AppTheme` in isolated tests — causes theme assertion failures
284
+ - Flaky `Thread.sleep()` for async waits — use `waitUntil` or `mainClock`
285
+
286
+ ---
287
+
288
+ ## Related Skills
289
+ - `compose-testing` — Compose-specific test APIs in depth
290
+ - `integration-testing` — testing layers below UI
291
+ - `unit-testing` — fast isolated tests for logic
292
+ - `hilt` — Hilt test setup
293
+ - `fake-data` — building consistent test data