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.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- 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
|