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,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: share-intent
|
|
3
|
+
description: >
|
|
4
|
+
Sharing content from Android apps using Intent and ShareSheet.
|
|
5
|
+
Load this skill when sharing text, URLs, images, or files with other
|
|
6
|
+
apps, customizing the share sheet, receiving shared content,
|
|
7
|
+
or implementing the Android Sharesheet API.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Share Intent
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Android's share system lets apps send and receive content through `Intent.ACTION_SEND`. The system displays a share sheet with compatible apps. Modern apps use the `ShareCompat` API or `rememberLauncherForActivityResult` in Compose for a cleaner integration.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use `ShareCompat.IntentBuilder` — not raw `Intent` — for outgoing shares
|
|
20
|
+
- Always use `FileProvider` for sharing files — direct `file://` URIs are blocked on Android 7+
|
|
21
|
+
- Handle incoming share intents in `onNewIntent` as well as `onCreate`
|
|
22
|
+
- Validate incoming share content — treat it as untrusted input
|
|
23
|
+
- Use `ChooserIntent` to customize the share sheet title
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Sharing Text and URLs
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Share plain text or URL
|
|
31
|
+
fun shareText(context: Context, text: String, title: String = "Share via") {
|
|
32
|
+
val intent = ShareCompat.IntentBuilder(context)
|
|
33
|
+
.setType("text/plain")
|
|
34
|
+
.setText(text)
|
|
35
|
+
.setChooserTitle(title)
|
|
36
|
+
.createChooserIntent()
|
|
37
|
+
|
|
38
|
+
context.startActivity(intent)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ✅ Share URL with subject
|
|
42
|
+
fun shareUrl(context: Context, url: String, subject: String) {
|
|
43
|
+
val intent = ShareCompat.IntentBuilder(context)
|
|
44
|
+
.setType("text/plain")
|
|
45
|
+
.setText(url)
|
|
46
|
+
.setSubject(subject)
|
|
47
|
+
.setChooserTitle("Share link via")
|
|
48
|
+
.createChooserIntent()
|
|
49
|
+
|
|
50
|
+
context.startActivity(intent)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Sharing Images
|
|
57
|
+
|
|
58
|
+
```kotlin
|
|
59
|
+
// ✅ Share image via FileProvider
|
|
60
|
+
fun shareImage(context: Context, imageFile: File) {
|
|
61
|
+
val uri = FileProvider.getUriForFile(
|
|
62
|
+
context,
|
|
63
|
+
"${context.packageName}.fileprovider",
|
|
64
|
+
imageFile
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
val intent = ShareCompat.IntentBuilder(context)
|
|
68
|
+
.setType("image/jpeg")
|
|
69
|
+
.setStream(uri)
|
|
70
|
+
.setChooserTitle("Share image via")
|
|
71
|
+
.createChooserIntent()
|
|
72
|
+
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
73
|
+
|
|
74
|
+
context.startActivity(intent)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## FileProvider Setup
|
|
81
|
+
|
|
82
|
+
```xml
|
|
83
|
+
<!-- AndroidManifest.xml -->
|
|
84
|
+
<provider
|
|
85
|
+
android:name="androidx.core.content.FileProvider"
|
|
86
|
+
android:authorities="${applicationId}.fileprovider"
|
|
87
|
+
android:exported="false"
|
|
88
|
+
android:grantUriPermissions="true">
|
|
89
|
+
<meta-data
|
|
90
|
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
91
|
+
android:resource="@xml/file_paths" />
|
|
92
|
+
</provider>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```xml
|
|
96
|
+
<!-- res/xml/file_paths.xml -->
|
|
97
|
+
<paths>
|
|
98
|
+
<cache-path name="cache" path="." />
|
|
99
|
+
<files-path name="files" path="." />
|
|
100
|
+
<external-cache-path name="external_cache" path="." />
|
|
101
|
+
</paths>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Sharing in Compose
|
|
107
|
+
|
|
108
|
+
```kotlin
|
|
109
|
+
// ✅ Share from Compose
|
|
110
|
+
@Composable
|
|
111
|
+
fun ShareButton(text: String) {
|
|
112
|
+
val context = LocalContext.current
|
|
113
|
+
|
|
114
|
+
Button(onClick = {
|
|
115
|
+
shareText(context, text)
|
|
116
|
+
}) {
|
|
117
|
+
Icon(Icons.Default.Share, contentDescription = null)
|
|
118
|
+
Spacer(Modifier.width(8.dp))
|
|
119
|
+
Text("Share")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Receiving Shared Content
|
|
127
|
+
|
|
128
|
+
```kotlin
|
|
129
|
+
// ✅ Handle incoming share in Activity
|
|
130
|
+
class MainActivity : ComponentActivity() {
|
|
131
|
+
|
|
132
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
133
|
+
super.onCreate(savedInstanceState)
|
|
134
|
+
handleIncomingShare(intent)
|
|
135
|
+
setContent { AppContent() }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override fun onNewIntent(intent: Intent) {
|
|
139
|
+
super.onNewIntent(intent)
|
|
140
|
+
handleIncomingShare(intent)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun handleIncomingShare(intent: Intent?) {
|
|
144
|
+
if (intent?.action != Intent.ACTION_SEND) return
|
|
145
|
+
|
|
146
|
+
when {
|
|
147
|
+
intent.type == "text/plain" -> {
|
|
148
|
+
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return
|
|
149
|
+
// handle text
|
|
150
|
+
}
|
|
151
|
+
intent.type?.startsWith("image/") == true -> {
|
|
152
|
+
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) ?: return
|
|
153
|
+
// handle image URI
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Register in manifest to receive shares
|
|
160
|
+
// <intent-filter>
|
|
161
|
+
// <action android:name="android.intent.action.SEND" />
|
|
162
|
+
// <category android:name="android.intent.category.DEFAULT" />
|
|
163
|
+
// <data android:mimeType="text/plain" />
|
|
164
|
+
// <data android:mimeType="image/*" />
|
|
165
|
+
// </intent-filter>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Anti-Patterns
|
|
171
|
+
|
|
172
|
+
- Using `file://` URI directly in share intent — blocked on Android 7+ (FileUriExposedException)
|
|
173
|
+
- Not granting `FLAG_GRANT_READ_URI_PERMISSION` for FileProvider URIs — receiving app can't read
|
|
174
|
+
- Not validating incoming shared content — treat as untrusted
|
|
175
|
+
- Using `startActivity` without a chooser for text sharing — opens default app silently
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Related Skills
|
|
180
|
+
- `filesystem` — writing files to share via FileProvider
|
|
181
|
+
- `manifest` — intent filter for receiving shares
|
|
182
|
+
- `deep-link` — handling incoming intents
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compose-testing
|
|
3
|
+
description: >
|
|
4
|
+
Jetpack Compose UI testing APIs and patterns.
|
|
5
|
+
Load this skill when using ComposeTestRule, finding nodes by semantics,
|
|
6
|
+
testing animations, asserting accessibility properties, verifying
|
|
7
|
+
recomposition behavior, or debugging failing Compose tests.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Compose Testing
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Compose Testing uses a semantic tree — a parallel representation of the UI — to find and interact with composables. Unlike Espresso's view hierarchy, Compose tests work through `SemanticsNode`s which expose accessibility properties. Tests are deterministic because `ComposeTestRule` controls the clock.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Tests interact through the **semantic tree** — not the visual layout
|
|
20
|
+
- **`testTag`** is the most reliable locator — use for non-text UI elements
|
|
21
|
+
- **`contentDescription`** serves double duty — accessibility and testability
|
|
22
|
+
- `ComposeTestRule` **controls the clock** — animations and async are deterministic
|
|
23
|
+
- Compose tests are **synchronous by default** — the rule awaits idle state after each action
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Test Rule Setup
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ For isolated composable tests (no Activity needed)
|
|
31
|
+
@get:Rule
|
|
32
|
+
val composeRule = createComposeRule()
|
|
33
|
+
|
|
34
|
+
// ✅ For full Activity/NavHost tests
|
|
35
|
+
@get:Rule
|
|
36
|
+
val composeRule = createAndroidComposeRule<MainActivity>()
|
|
37
|
+
|
|
38
|
+
// ✅ For Hilt + Activity tests
|
|
39
|
+
@HiltAndroidTest
|
|
40
|
+
class MyScreenTest {
|
|
41
|
+
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
|
|
42
|
+
@get:Rule(order = 1) val composeRule = createAndroidComposeRule<MainActivity>()
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Semantic Matchers Reference
|
|
49
|
+
|
|
50
|
+
```kotlin
|
|
51
|
+
// ── Text ──────────────────────────────────────────────────────────────────
|
|
52
|
+
hasText("Submit") // exact match
|
|
53
|
+
hasText("ubmit", substring = true) // substring
|
|
54
|
+
hasText("submit", ignoreCase = true) // case-insensitive
|
|
55
|
+
|
|
56
|
+
// ── Tags ──────────────────────────────────────────────────────────────────
|
|
57
|
+
hasTestTag("my_button")
|
|
58
|
+
|
|
59
|
+
// ── Accessibility ─────────────────────────────────────────────────────────
|
|
60
|
+
hasContentDescription("Back")
|
|
61
|
+
hasContentDescription("icon", substring = true)
|
|
62
|
+
|
|
63
|
+
// ── Role / Type ───────────────────────────────────────────────────────────
|
|
64
|
+
isDialog()
|
|
65
|
+
isPopup()
|
|
66
|
+
isFocused()
|
|
67
|
+
isEnabled()
|
|
68
|
+
isNotEnabled()
|
|
69
|
+
hasClickAction()
|
|
70
|
+
hasScrollAction()
|
|
71
|
+
isSelected()
|
|
72
|
+
isToggleable()
|
|
73
|
+
isOn()
|
|
74
|
+
isOff()
|
|
75
|
+
|
|
76
|
+
// ── Hierarchy ─────────────────────────────────────────────────────────────
|
|
77
|
+
hasParent(hasTestTag("card"))
|
|
78
|
+
hasAnyChild(hasText("Ali"))
|
|
79
|
+
hasAnySibling(hasText("Submit"))
|
|
80
|
+
|
|
81
|
+
// ── Combining ─────────────────────────────────────────────────────────────
|
|
82
|
+
hasText("Ali") and isEnabled()
|
|
83
|
+
hasText("Ali") or hasTestTag("ali_node")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Finding Nodes
|
|
89
|
+
|
|
90
|
+
```kotlin
|
|
91
|
+
// Single node — throws if 0 or 2+ found
|
|
92
|
+
composeRule.onNodeWithText("Submit")
|
|
93
|
+
composeRule.onNodeWithTag("loading_indicator")
|
|
94
|
+
composeRule.onNodeWithContentDescription("Close")
|
|
95
|
+
composeRule.onNode(hasText("Ali") and isEnabled())
|
|
96
|
+
|
|
97
|
+
// Multiple nodes
|
|
98
|
+
composeRule.onAllNodesWithText("Item")
|
|
99
|
+
composeRule.onAllNodesWithTag("list_item")
|
|
100
|
+
composeRule.onAllNodes(hasClickAction())
|
|
101
|
+
|
|
102
|
+
// Indexed access
|
|
103
|
+
composeRule.onAllNodesWithTag("list_item")[0]
|
|
104
|
+
composeRule.onAllNodesWithTag("list_item").onFirst()
|
|
105
|
+
composeRule.onAllNodesWithTag("list_item").onLast()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Actions
|
|
111
|
+
|
|
112
|
+
```kotlin
|
|
113
|
+
val node = composeRule.onNodeWithTag("my_button")
|
|
114
|
+
|
|
115
|
+
// Click
|
|
116
|
+
node.performClick()
|
|
117
|
+
node.performLongClick()
|
|
118
|
+
node.performDoubleClick()
|
|
119
|
+
|
|
120
|
+
// Text input
|
|
121
|
+
node.performTextInput("Hello")
|
|
122
|
+
node.performTextClearance()
|
|
123
|
+
node.performTextReplacement("New text")
|
|
124
|
+
|
|
125
|
+
// Scroll
|
|
126
|
+
node.performScrollTo() // scroll this node into view
|
|
127
|
+
node.performScrollToIndex(10) // scroll LazyList to index
|
|
128
|
+
node.performScrollToKey("item_key") // scroll LazyList to keyed item
|
|
129
|
+
node.performScrollToNode(hasText("Target"))
|
|
130
|
+
|
|
131
|
+
// Gestures
|
|
132
|
+
node.performTouchInput { swipeLeft() }
|
|
133
|
+
node.performTouchInput { swipeUp() }
|
|
134
|
+
node.performTouchInput { click(center) }
|
|
135
|
+
|
|
136
|
+
// IME
|
|
137
|
+
node.performImeAction()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Assertions
|
|
143
|
+
|
|
144
|
+
```kotlin
|
|
145
|
+
val node = composeRule.onNodeWithTag("result_text")
|
|
146
|
+
|
|
147
|
+
// Existence
|
|
148
|
+
node.assertExists()
|
|
149
|
+
node.assertDoesNotExist()
|
|
150
|
+
|
|
151
|
+
// Visibility
|
|
152
|
+
node.assertIsDisplayed()
|
|
153
|
+
node.assertIsNotDisplayed()
|
|
154
|
+
|
|
155
|
+
// State
|
|
156
|
+
node.assertIsEnabled()
|
|
157
|
+
node.assertIsNotEnabled()
|
|
158
|
+
node.assertIsFocused()
|
|
159
|
+
node.assertIsSelected()
|
|
160
|
+
node.assertIsOn()
|
|
161
|
+
node.assertIsOff()
|
|
162
|
+
|
|
163
|
+
// Content
|
|
164
|
+
node.assertTextEquals("Exact text")
|
|
165
|
+
node.assertTextContains("partial")
|
|
166
|
+
node.assertContentDescriptionEquals("desc")
|
|
167
|
+
node.assertHasClickAction()
|
|
168
|
+
|
|
169
|
+
// Count
|
|
170
|
+
composeRule.onAllNodesWithTag("item").assertCountEquals(3)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Testing State Changes
|
|
176
|
+
|
|
177
|
+
```kotlin
|
|
178
|
+
// ✅ Test that UI updates when state changes
|
|
179
|
+
@Test
|
|
180
|
+
fun showsErrorAfterFailedLoad() {
|
|
181
|
+
var state: UserListUiState by mutableStateOf(UserListUiState.Loading)
|
|
182
|
+
|
|
183
|
+
composeRule.setContent {
|
|
184
|
+
AppTheme {
|
|
185
|
+
UserListContent(state = state, onUserClick = {})
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Initial loading state
|
|
190
|
+
composeRule.onNodeWithTag("loading_indicator").assertIsDisplayed()
|
|
191
|
+
|
|
192
|
+
// Update state
|
|
193
|
+
state = UserListUiState.Error("Network failed")
|
|
194
|
+
|
|
195
|
+
// Verify UI updated
|
|
196
|
+
composeRule.onNodeWithText("Network failed").assertIsDisplayed()
|
|
197
|
+
composeRule.onNodeWithTag("loading_indicator").assertDoesNotExist()
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Clock Control and Animations
|
|
204
|
+
|
|
205
|
+
```kotlin
|
|
206
|
+
// ✅ Disable auto-advance for manual clock control
|
|
207
|
+
composeRule.mainClock.autoAdvance = false
|
|
208
|
+
|
|
209
|
+
// Advance by specific duration
|
|
210
|
+
composeRule.mainClock.advanceTimeBy(300L)
|
|
211
|
+
|
|
212
|
+
// Advance until idle
|
|
213
|
+
composeRule.mainClock.advanceTimeUntilIdle()
|
|
214
|
+
|
|
215
|
+
// ✅ Test animated visibility
|
|
216
|
+
@Test
|
|
217
|
+
fun animatedContentAppearsAfterDelay() {
|
|
218
|
+
var visible by mutableStateOf(false)
|
|
219
|
+
|
|
220
|
+
composeRule.setContent {
|
|
221
|
+
AnimatedVisibility(visible = visible) {
|
|
222
|
+
Text("Hello", modifier = Modifier.testTag("animated_text"))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
composeRule.onNodeWithTag("animated_text").assertDoesNotExist()
|
|
227
|
+
|
|
228
|
+
visible = true
|
|
229
|
+
composeRule.mainClock.advanceTimeBy(500L)
|
|
230
|
+
|
|
231
|
+
composeRule.onNodeWithTag("animated_text").assertIsDisplayed()
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Debugging Failing Tests
|
|
238
|
+
|
|
239
|
+
```kotlin
|
|
240
|
+
// ✅ Print semantic tree to understand what's available
|
|
241
|
+
composeRule.onRoot().printToLog("TEST_TAG")
|
|
242
|
+
|
|
243
|
+
// ✅ Print subtree
|
|
244
|
+
composeRule.onNodeWithTag("user_card").printToLog("CARD_TREE")
|
|
245
|
+
|
|
246
|
+
// Output shows full semantic tree:
|
|
247
|
+
// Node #1 at (0.0, 0.0, 411.4, 72.7)px
|
|
248
|
+
// |-Node #2 at (16.0, 12.0, 395.4, 60.7)px
|
|
249
|
+
// Text = "Ali Rezaei"
|
|
250
|
+
// Actions = [OnClick, GetTextLayoutResult]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## waitUntil
|
|
256
|
+
|
|
257
|
+
```kotlin
|
|
258
|
+
// ✅ Wait for async UI change (max timeout)
|
|
259
|
+
composeRule.waitUntil(timeoutMillis = 5_000) {
|
|
260
|
+
composeRule
|
|
261
|
+
.onAllNodesWithTag("user_item")
|
|
262
|
+
.fetchSemanticsNodes()
|
|
263
|
+
.isNotEmpty()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ✅ Wait for node to appear
|
|
267
|
+
composeRule.waitUntilAtLeastOneExists(
|
|
268
|
+
hasTestTag("user_item"),
|
|
269
|
+
timeoutMillis = 5_000
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// ✅ Wait for node to disappear
|
|
273
|
+
composeRule.waitUntilDoesNotExist(
|
|
274
|
+
hasTestTag("loading_indicator"),
|
|
275
|
+
timeoutMillis = 5_000
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Anti-Patterns
|
|
282
|
+
|
|
283
|
+
- Using `Thread.sleep()` for async waits — use `waitUntil` or let the test rule handle idle
|
|
284
|
+
- Locating nodes by position index in UI — fragile; use `testTag` or content
|
|
285
|
+
- Not wrapping test content in `AppTheme` — may fail if theme-dependent assertions are used
|
|
286
|
+
- Asserting on implementation-level composable names — use semantic properties instead
|
|
287
|
+
- `autoAdvance = false` without remembering to advance — test hangs indefinitely
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Related Skills
|
|
292
|
+
- `ui-testing` — full screen UI tests with Activity
|
|
293
|
+
- `compose` — Compose fundamentals including `testTag` placement
|
|
294
|
+
- `snapshot-testing` — screenshot-based regression testing
|
|
295
|
+
- `espresso` — View-based UI testing
|
|
296
|
+
- `accessibility` — semantic properties used in both testing and accessibility
|