@buivietphi/skill-mobile-mt 1.4.1 → 1.4.3

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.

Potentially problematic release.


This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.

package/AGENTS.md CHANGED
@@ -40,7 +40,7 @@ skill-mobile-mt/
40
40
  │ └── ios-native.md ← Swift + UIKit/SwiftUI patterns (1,452 tokens)
41
41
 
42
42
  ├── android/
43
- │ └── android-native.md ← Kotlin + Compose patterns (2,100 tokens)
43
+ │ └── android-native.md ← Kotlin + Compose + Java legacy patterns (4,400 tokens)
44
44
 
45
45
  └── shared/
46
46
 
@@ -62,6 +62,8 @@ skill-mobile-mt/
62
62
  ├── release-checklist.md ← App Store/Play Store checklist (587 tokens)
63
63
 
64
64
  ├── offline-first.md ← Local-first + sync patterns (2,566 tokens)
65
+ ├── testing-strategy.md ← Detox + Maestro + XCUITest + Espresso E2E (2,200 tokens)
66
+ ├── ci-cd.md ← GitHub Actions CI templates (2,500 tokens)
65
67
 
66
68
  ├── ── TEMPLATES (copy to your project) ────────────────────
67
69
  ├── claude-md-template.md ← CLAUDE.md for Claude Code (copy to project root)
@@ -69,8 +71,8 @@ skill-mobile-mt/
69
71
  ```
70
72
 
71
73
  **Token totals:**
72
- - Smart load (1 platform + core shared): **~38,600 tokens** (30.2% of 128K)
73
- - Full load (all files): **~70,000 tokens** (54.7% of 128K)
74
+ - Smart load (1 platform + core shared): **~40,900 tokens** (31.9% of 128K)
75
+ - Full load (all files): **~77,000 tokens** (60.2% of 128K)
74
76
 
75
77
  ---
76
78
 
@@ -109,6 +111,8 @@ The agent reads the task, then decides which extra file to load:
109
111
  | "Install this package / upgrade SDK" | `shared/version-management.md` |
110
112
  | "Prepare for App Store / Play Store" | `shared/release-checklist.md` |
111
113
  | "Weird issue, not sure why" | `shared/common-pitfalls.md` |
114
+ | "Write / run E2E tests" | `shared/testing-strategy.md` |
115
+ | "Setup CI/CD / GitHub Actions" | `shared/ci-cd.md` |
112
116
 
113
117
  **Load cost:** +500 to +3,500 tokens per on-demand file.
114
118
 
@@ -118,7 +122,7 @@ The agent reads the task, then decides which extra file to load:
118
122
 
119
123
  **No automatic trigger.** Full load happens when the AI reads every file without being selective — either because it's over-eager, or because the user explicitly asks for it.
120
124
 
121
- **Total:** ~70,000 tokens (54.7% of 128K, 35% of 200K)
125
+ **Total:** ~72,300 tokens (56.5% of 128K, 36% of 200K)
122
126
 
123
127
  **How it actually works:**
124
128
  - `@skill-mobile-mt` only injects SKILL.md into context
@@ -143,7 +147,7 @@ The agent reads the task, then decides which extra file to load:
143
147
  ```yaml
144
148
  skill:
145
149
  name: skill-mobile-mt
146
- version: "1.4.0"
150
+ version: "1.4.2"
147
151
  author: buivietphi
148
152
  category: engineering
149
153
  tags:
@@ -210,9 +214,9 @@ skill:
210
214
  java: ".java files in app/src/"
211
215
 
212
216
  context_budget:
213
- max_tokens: 70000
214
- smart_load_tokens: 38600
215
- savings: "~45%"
217
+ max_tokens: 77000
218
+ smart_load_tokens: 40900
219
+ savings: "~47%"
216
220
  ```
217
221
 
218
222
  ---
@@ -279,6 +283,8 @@ Priority 6 (ON-DEMAND): shared/observability.md — Sessions as 4th pillar
279
283
  Priority 6 (ON-DEMAND): shared/document-analysis.md — Parse images/PDFs → code
280
284
  Priority 6 (ON-DEMAND): shared/release-checklist.md — Pre-release verification
281
285
  Priority 6 (ON-DEMAND): shared/common-pitfalls.md — Known issue patterns
286
+ Priority 6 (ON-DEMAND): shared/testing-strategy.md — Detox + Maestro + XCUITest + Espresso E2E
287
+ Priority 6 (ON-DEMAND): shared/ci-cd.md — GitHub Actions CI/CD templates
282
288
  ```
283
289
 
284
290
  ---
@@ -457,10 +463,10 @@ npx @buivietphi/skill-mobile-mt --init all # → all files
457
463
  {
458
464
  "id": "skill-mobile-mt",
459
465
  "name": "skill-mobile-mt",
460
- "version": "1.4.0",
466
+ "version": "1.4.3",
461
467
  "author": "buivietphi",
462
468
  "category": "engineering",
463
- "description": "Master Senior Mobile Engineer. Pre-built patterns from 18 production apps + project adaptation. Auto-detects language and framework. React Native, Flutter, iOS, Android.",
469
+ "description": "Master Senior Mobile Engineer. Pre-built patterns from 30+ production repos + project adaptation. Auto-detects language and framework. React Native, Flutter, iOS, Android.",
464
470
  "risk": "low",
465
471
  "source": "buivietphi (MIT)",
466
472
  "platforms": ["react-native", "flutter", "ios", "android"],
package/README.md CHANGED
@@ -72,6 +72,73 @@ Reads your current project first, then follows **your** conventions:
72
72
  - Clones the most similar existing feature when scaffolding new ones
73
73
  - Never suggests migrations or imposes different architecture
74
74
 
75
+ ## Humanizer — Mobile Copy
76
+
77
+ Installed automatically alongside the main skill. Removes AI writing patterns from mobile app text.
78
+
79
+ ```
80
+ @humanizer-mobile
81
+ ```
82
+
83
+ Use for: app store descriptions, release notes, error messages, onboarding, push notifications, permission prompts, paywall copy.
84
+
85
+ **App Store Description**
86
+ ```
87
+ ❌ "TaskFlow is a powerful, comprehensive task management application that
88
+ seamlessly integrates with your existing workflow."
89
+
90
+ ✅ "TaskFlow keeps your team's work in one place. Add tasks, assign them,
91
+ set deadlines — everything syncs in real time. Works with Slack and Notion."
92
+ ```
93
+
94
+ **Release Notes**
95
+ ```
96
+ ❌ "Version 2.1.0 introduces significant enhancements to the overall user
97
+ experience including robust improvements to performance and reliability."
98
+
99
+ ✅ "2.1.0
100
+ - Faster load times on older Android devices (was 4s, now 1.2s)
101
+ - Fixed crash when opening notifications while offline
102
+ - Dark mode now remembers your setting between sessions"
103
+ ```
104
+
105
+ **Error Messages**
106
+ ```
107
+ ❌ "We apologize for the inconvenience. An unexpected error has occurred."
108
+
109
+ ✅ "Couldn't save — no internet connection.
110
+ [Try again] [Save for later]"
111
+ ```
112
+
113
+ **Push Notifications**
114
+ ```
115
+ ❌ "You have received a new message from a team member."
116
+ ✅ "Alex commented on "Landing page redesign""
117
+ ```
118
+
119
+ **Paywall**
120
+ ```
121
+ ❌ "Unlock the full potential of our comprehensive premium features."
122
+
123
+ ✅ "Go Pro
124
+ • Unlimited projects (free plan: 3)
125
+ • Team sharing
126
+ [Start 7-day free trial]"
127
+ ```
128
+
129
+ Covers 24 AI patterns across: content inflation, chatbot artifacts, style issues, mobile-specific copy (App Store limits, permission prompts, rating requests, subscription CTAs).
130
+
131
+ ### Works with all agents
132
+
133
+ | Agent | Invocation |
134
+ |-------|-----------|
135
+ | Claude Code | `@humanizer-mobile` |
136
+ | Gemini CLI | `@humanizer-mobile` |
137
+ | Codex / Kimi / Antigravity | `@humanizer-mobile` |
138
+ | Cline / Roo Code / Cursor / Windsurf / Copilot | Paste the text and say "humanize this app copy" |
139
+
140
+ ---
141
+
75
142
  ## Quick Start Examples
76
143
 
77
144
  ### Step 1: Install the skill
@@ -101,6 +168,15 @@ You'll see an interactive checkbox — use arrow keys to navigate, space to sele
101
168
  ◯ Antigravity [not found]
102
169
  ```
103
170
 
171
+ Install output:
172
+
173
+ ```
174
+ ✓ skill-mobile-mt/ → Claude Code (~/.claude/skills/skill-mobile-mt)
175
+ ✓ humanizer-mobile/ → Claude Code (~/.claude/skills/humanizer-mobile)
176
+ ```
177
+
178
+ Both are installed automatically. Use `@humanizer-mobile` for app store copy, release notes, and UX text.
179
+
104
180
  ### Step 2: Generate project rules
105
181
 
106
182
  ```bash
@@ -231,9 +307,9 @@ iOS only?
231
307
  |----------|-------:|----------:|----------:|
232
308
  | SKILL.md only | ~13,200 | 10.3% | 6.6% |
233
309
  | + 1 platform + core shared/ | ~38,600 | 30.2% | 19.3% |
234
- | Cross-platform (RN/Flutter + iOS + Android) | ~53,000 | 41.4% | 26.5% |
235
- | All files loaded | ~70,000 | 54.7% | 35.0% |
236
- | **Smart load (recommended)** | **~38,600** | **30.2%** | **19.3%** |
310
+ | Cross-platform (RN/Flutter + iOS + Android) | ~55,300 | 43.2% | 27.7% |
311
+ | All files loaded | ~77,000 | 60.2% | 38.5% |
312
+ | **Smart load (recommended)** | **~40,900** | **31.9%** | **20.5%** |
237
313
 
238
314
  ### Per-file token breakdown
239
315
 
@@ -244,7 +320,7 @@ iOS only?
244
320
  | `react-native/react-native.md` | 5,108 |
245
321
  | `flutter/flutter.md` | 2,100 |
246
322
  | `ios/ios-native.md` | 1,452 |
247
- | `android/android-native.md` | 2,100 |
323
+ | `android/android-native.md` | 4,400 |
248
324
  | `shared/code-review.md` | 865 |
249
325
  | `shared/bug-detection.md` | 499 |
250
326
  | `shared/prompt-engineering.md` | 3,927 |
@@ -259,9 +335,11 @@ iOS only?
259
335
  | `shared/version-management.md` | 3,500 |
260
336
  | `shared/observability.md` | 3,000 |
261
337
  | `shared/offline-first.md` | 2,566 |
338
+ | `shared/testing-strategy.md` | 2,200 |
339
+ | `shared/ci-cd.md` | 2,500 |
262
340
  | `shared/claude-md-template.md` | ~500 |
263
341
  | `shared/agent-rules-template.md` | ~2,500 |
264
- | **Total** | **~48,800** |
342
+ | **Total** | **~55,800** |
265
343
 
266
344
  ## Installed Structure
267
345
 
@@ -277,7 +355,7 @@ iOS only?
277
355
  ├── ios/
278
356
  │ └── ios-native.md iOS Swift patterns
279
357
  ├── android/
280
- │ └── android-native.md Android Kotlin patterns
358
+ │ └── android-native.md Android Kotlin + Java patterns
281
359
  └── shared/
282
360
  ├── code-review.md Senior review checklist
283
361
  ├── bug-detection.md Auto bug scanner
@@ -293,6 +371,8 @@ iOS only?
293
371
  ├── observability.md Sessions as 4th pillar
294
372
  ├── release-checklist.md Pre-release verification
295
373
  ├── offline-first.md Local-first + sync patterns
374
+ ├── testing-strategy.md Detox + Maestro + XCUITest + Espresso E2E
375
+ ├── ci-cd.md GitHub Actions CI/CD templates
296
376
  ├── claude-md-template.md CLAUDE.md template for projects
297
377
  └── agent-rules-template.md Rules templates for all agents
298
378
  ```
@@ -423,66 +503,6 @@ your-project/
423
503
  | **Kiro** | Reads `.kiro/steering/` from project root | `npx skill-mobile-mt --init kiro` |
424
504
  | **Antigravity** | Reads from `~/.agents/skills/` | `npx skill-mobile-mt --antigravity` |
425
505
 
426
- ## Humanizer — Mobile Copy
427
-
428
- Installed automatically alongside the main skill. Removes AI writing patterns from mobile app text.
429
-
430
- ```
431
- @humanizer-mobile
432
- ```
433
-
434
- Use for: app store descriptions, release notes, error messages, onboarding, push notifications, permission prompts, paywall copy.
435
-
436
- ### Examples
437
-
438
- **App Store Description**
439
- ```
440
- ❌ "TaskFlow is a powerful, comprehensive task management application that
441
- seamlessly integrates with your existing workflow."
442
-
443
- ✅ "TaskFlow keeps your team's work in one place. Add tasks, assign them,
444
- set deadlines — everything syncs in real time. Works with Slack and Notion."
445
- ```
446
-
447
- **Release Notes**
448
- ```
449
- ❌ "Version 2.1.0 introduces significant enhancements to the overall user
450
- experience including robust improvements to performance and reliability."
451
-
452
- ✅ "2.1.0
453
- - Faster load times on older Android devices (was 4s, now 1.2s)
454
- - Fixed crash when opening notifications while offline
455
- - Dark mode now remembers your setting between sessions"
456
- ```
457
-
458
- **Error Messages**
459
- ```
460
- ❌ "We apologize for the inconvenience. An unexpected error has occurred."
461
-
462
- ✅ "Couldn't save — no internet connection.
463
- [Try again] [Save for later]"
464
- ```
465
-
466
- **Push Notifications**
467
- ```
468
- ❌ "You have received a new message from a team member."
469
- ✅ "Alex commented on "Landing page redesign""
470
- ```
471
-
472
- **Paywall**
473
- ```
474
- ❌ "Unlock the full potential of our comprehensive premium features."
475
-
476
- ✅ "Go Pro
477
- • Unlimited projects (free plan: 3)
478
- • Team sharing
479
- [Start 7-day free trial]"
480
- ```
481
-
482
- Covers 24 AI patterns across: content inflation, chatbot artifacts, style issues, mobile-specific copy (App Store limits, permission prompts, rating requests, subscription CTAs).
483
-
484
- ---
485
-
486
506
  ## License
487
507
 
488
508
  MIT — by [buivietphi](https://github.com/buivietphi)
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: skill-mobile-mt
3
3
  description: "Master Senior Mobile Engineer. Patterns from 30+ production repos (200k+ GitHub stars: Ignite, Expensify, Mattermost, Immich, AppFlowy, Now in Android, TCA). Use when: building mobile features, fixing mobile bugs, reviewing mobile code, mobile architecture, React Native, Flutter, iOS Swift, Android Kotlin, mobile performance, mobile security audit, mobile code review, app release. Two modes: (1) default = pre-built production patterns, (2) 'project' = reads current project and adapts."
4
- version: "1.4.1"
4
+ version: "1.4.3"
5
5
  author: buivietphi
6
6
  priority: high
7
7
  user-invocable: true
@@ -130,6 +130,12 @@ USER REQUEST → ACTION (Read tool required)
130
130
  "Offline / cache / sync" → Read: shared/offline-first.md
131
131
  then: implement local-first architecture
132
132
 
133
+ "Write/run E2E tests" → Read: shared/testing-strategy.md
134
+ then: Detox (RN) or Maestro (cross-platform) or XCUITest/Espresso
135
+
136
+ "Setup CI/CD / GitHub Actions" → Read: shared/ci-cd.md
137
+ then: test → build → distribute pipeline
138
+
133
139
  ```
134
140
 
135
141
  **⛔ NEVER start coding without identifying the task type first.**
@@ -254,5 +254,227 @@ dependencies {
254
254
 
255
255
  ---
256
256
 
257
+ ## Java Interop (Legacy Projects)
258
+
259
+ > For projects still using Java. New code should use Kotlin.
260
+
261
+ ### Activity + XML Layout (Java)
262
+
263
+ ```java
264
+ // MainActivity.java
265
+ @AndroidEntryPoint
266
+ public class MainActivity extends AppCompatActivity {
267
+
268
+ private MainViewModel viewModel;
269
+ private ActivityMainBinding binding;
270
+
271
+ @Override
272
+ protected void onCreate(Bundle savedInstanceState) {
273
+ super.onCreate(savedInstanceState);
274
+ binding = ActivityMainBinding.inflate(getLayoutInflater());
275
+ setContentView(binding.getRoot());
276
+
277
+ viewModel = new ViewModelProvider(this).get(MainViewModel.class);
278
+
279
+ // Observe LiveData (Java equivalent of collectAsStateWithLifecycle)
280
+ viewModel.getUiState().observe(this, state -> {
281
+ if (state instanceof UiState.Loading) {
282
+ binding.progressBar.setVisibility(View.VISIBLE);
283
+ binding.recyclerView.setVisibility(View.GONE);
284
+ } else if (state instanceof UiState.Error) {
285
+ binding.progressBar.setVisibility(View.GONE);
286
+ Toast.makeText(this, ((UiState.Error) state).getMessage(), Toast.LENGTH_SHORT).show();
287
+ } else if (state instanceof UiState.Success) {
288
+ binding.progressBar.setVisibility(View.GONE);
289
+ binding.recyclerView.setVisibility(View.VISIBLE);
290
+ adapter.submitList(((UiState.Success<List<Product>>) state).getData());
291
+ }
292
+ });
293
+
294
+ binding.retryButton.setOnClickListener(v -> viewModel.load());
295
+ }
296
+ }
297
+ ```
298
+
299
+ ### ViewModel + LiveData (Java)
300
+
301
+ ```java
302
+ // ProductListViewModel.java
303
+ @HiltViewModel
304
+ public class ProductListViewModel extends ViewModel {
305
+
306
+ private final MutableLiveData<UiState<List<Product>>> _uiState =
307
+ new MutableLiveData<>(new UiState.Loading<>());
308
+ private final LiveData<UiState<List<Product>>> uiState = _uiState;
309
+
310
+ private final GetProductsUseCase getProducts;
311
+
312
+ @Inject
313
+ public ProductListViewModel(GetProductsUseCase getProducts) {
314
+ this.getProducts = getProducts;
315
+ load();
316
+ }
317
+
318
+ public LiveData<UiState<List<Product>>> getUiState() { return uiState; }
319
+
320
+ public void load() {
321
+ _uiState.setValue(new UiState.Loading<>());
322
+ // Use RxJava or ExecutorService for async in Java
323
+ ExecutorService executor = Executors.newSingleThreadExecutor();
324
+ executor.execute(() -> {
325
+ try {
326
+ List<Product> items = getProducts.executeSync(); // blocking call
327
+ new Handler(Looper.getMainLooper()).post(() -> {
328
+ if (items.isEmpty()) _uiState.setValue(new UiState.Empty<>());
329
+ else _uiState.setValue(new UiState.Success<>(items));
330
+ });
331
+ } catch (Exception e) {
332
+ new Handler(Looper.getMainLooper()).post(() ->
333
+ _uiState.setValue(new UiState.Error<>(e.getMessage()))
334
+ );
335
+ } finally {
336
+ executor.shutdown();
337
+ }
338
+ });
339
+ }
340
+ }
341
+ ```
342
+
343
+ ### Retrofit + Callback (Java)
344
+
345
+ ```java
346
+ // ProductApi.java
347
+ public interface ProductApi {
348
+ @GET("products")
349
+ Call<ApiResponse<List<ProductDto>>> getProducts();
350
+ }
351
+
352
+ // ProductRepositoryImpl.java
353
+ public class ProductRepositoryImpl implements ProductRepository {
354
+ private final ProductApi api;
355
+ private final ProductDao dao;
356
+
357
+ @Inject
358
+ public ProductRepositoryImpl(ProductApi api, ProductDao dao) {
359
+ this.api = api;
360
+ this.dao = dao;
361
+ }
362
+
363
+ @Override
364
+ public void getProducts(Callback<List<Product>> callback) {
365
+ api.getProducts().enqueue(new retrofit2.Callback<ApiResponse<List<ProductDto>>>() {
366
+ @Override
367
+ public void onResponse(Call<ApiResponse<List<ProductDto>>> call,
368
+ Response<ApiResponse<List<ProductDto>>> response) {
369
+ if (response.isSuccessful() && response.body() != null) {
370
+ List<Product> items = mapToDomain(response.body().getData());
371
+ callback.onSuccess(items);
372
+ } else {
373
+ callback.onError("Server error: " + response.code());
374
+ }
375
+ }
376
+
377
+ @Override
378
+ public void onFailure(Call<ApiResponse<List<ProductDto>>> call, Throwable t) {
379
+ callback.onError(t.getMessage());
380
+ }
381
+ });
382
+ }
383
+ }
384
+ ```
385
+
386
+ ### Room DAO (Java)
387
+
388
+ ```java
389
+ // ProductDao.java
390
+ @Dao
391
+ public interface ProductDao {
392
+ @Query("SELECT * FROM products")
393
+ LiveData<List<ProductEntity>> getAll(); // LiveData for Java observers
394
+
395
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
396
+ void insertAll(List<ProductEntity> items);
397
+
398
+ @Query("DELETE FROM products")
399
+ void deleteAll();
400
+ }
401
+ ```
402
+
403
+ ### Java-Kotlin Interop Annotations
404
+
405
+ ```kotlin
406
+ // When writing Kotlin code that will be called from Java:
407
+
408
+ // @JvmStatic — allows calling companion object functions as static
409
+ class ProductMapper {
410
+ companion object {
411
+ @JvmStatic
412
+ fun toDomain(dto: ProductDto): Product = Product(dto.id, dto.name)
413
+ }
414
+ }
415
+ // Java: ProductMapper.toDomain(dto) ✅ (without @JvmStatic: ProductMapper.Companion.toDomain(dto))
416
+
417
+ // @JvmField — exposes Kotlin property as Java field (no getter/setter)
418
+ class Config {
419
+ companion object {
420
+ @JvmField val BASE_URL = "https://api.example.com"
421
+ }
422
+ }
423
+ // Java: Config.BASE_URL ✅
424
+
425
+ // @JvmOverloads — generates overloaded methods for default parameters
426
+ class ProductService @JvmOverloads constructor(
427
+ val baseUrl: String,
428
+ val timeout: Int = 30,
429
+ val retries: Int = 3,
430
+ )
431
+ // Java: new ProductService("url") ✅ (without: must pass all 3 params)
432
+
433
+ // @Throws — declares checked exceptions for Java callers
434
+ @Throws(IOException::class)
435
+ fun readFile(path: String): String { /* ... */ }
436
+ ```
437
+
438
+ ### Calling Kotlin Suspend from Java (Bridge Pattern)
439
+
440
+ ```java
441
+ // Use CoroutineScope from Java via CoroutinesInstrumentationHelper or wrapper
442
+ // ⚠️ Recommended: write a non-suspend wrapper in Kotlin
443
+
444
+ // Kotlin wrapper (bridge.kt)
445
+ object ProductBridge {
446
+ @JvmStatic
447
+ fun getProducts(scope: CoroutineScope, callback: ProductCallback) {
448
+ scope.launch {
449
+ try {
450
+ val items = getProductsUseCase()
451
+ callback.onSuccess(items)
452
+ } catch (e: Exception) {
453
+ callback.onError(e.message ?: "Error")
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ // Java side
460
+ ProductBridge.getProducts(lifecycleScope, new ProductCallback() {
461
+ @Override public void onSuccess(List<Product> items) { /* update UI */ }
462
+ @Override public void onError(String message) { /* show error */ }
463
+ });
464
+ ```
465
+
466
+ ### Java Common Pitfalls
467
+
468
+ | Pitfall | Fix |
469
+ |---------|-----|
470
+ | NullPointerException | `@NonNull` / `@Nullable` + null checks |
471
+ | Memory leak (Activity in async) | `WeakReference<Activity>` or cancel on `onDestroy` |
472
+ | Main thread network call | `Executors.newSingleThreadExecutor()` |
473
+ | No `Call.cancel()` | Store reference, cancel in `onDestroy` |
474
+ | Anonymous inner class holding outer ref | Use static inner class |
475
+
476
+ ---
477
+
257
478
  > Multi-module Gradle + Hilt + Compose + offline-first.
258
479
  > Clean Architecture with domain module having zero dependencies.
480
+ > Java legacy: use LiveData + Retrofit callbacks. Bridge Kotlin suspend with wrapper functions.
package/bin/install.mjs CHANGED
@@ -70,7 +70,7 @@ const fail = m => log(` ${c.red}✗${c.reset} ${m}`);
70
70
 
71
71
  function banner() {
72
72
  log(`\n${c.bold}${c.cyan} ┌──────────────────────────────────────────────────┐`);
73
- log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.1 │`);
73
+ log(` │ 📱 @buivietphi/skill-mobile-mt v1.4.3 │`);
74
74
  log(` │ Master Senior Mobile Engineer │`);
75
75
  log(` │ │`);
76
76
  log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buivietphi/skill-mobile-mt",
3
- "version": "1.4.1",
4
- "description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 18 production apps + local project adaptation. React Native, Flutter, iOS, Android. Supports Claude, Gemini, Kimi, Cursor, Copilot, Antigravity.",
3
+ "version": "1.4.3",
4
+ "description": "Master Senior Mobile Engineer skill for AI agents. Pre-built patterns from 30+ production repos. React Native, Flutter, iOS, Android. Supports Claude, Cline, Cursor, Windsurf, Copilot, Codex, Gemini, Kimi, Kilo Code, Kiro, Antigravity.",
5
5
  "author": "buivietphi",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -0,0 +1,423 @@
1
+ # CI/CD Pipelines — GitHub Actions for Mobile
2
+
3
+ > Automate: test → build → distribute. Never ship without CI.
4
+
5
+ ---
6
+
7
+ ## React Native — CI Pipeline
8
+
9
+ ```yaml
10
+ # .github/workflows/rn-ci.yml
11
+ name: React Native CI
12
+
13
+ on:
14
+ push:
15
+ branches: [main, develop]
16
+ pull_request:
17
+ branches: [main]
18
+
19
+ jobs:
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: '20'
28
+ cache: 'yarn' # or npm, pnpm
29
+
30
+ - name: Install dependencies
31
+ run: yarn install --frozen-lockfile
32
+
33
+ - name: TypeScript check
34
+ run: yarn tsc --noEmit
35
+
36
+ - name: Lint
37
+ run: yarn lint
38
+
39
+ - name: Unit tests
40
+ run: yarn test --ci --coverage --maxWorkers=2
41
+
42
+ - name: Upload coverage
43
+ uses: codecov/codecov-action@v4
44
+ with:
45
+ token: ${{ secrets.CODECOV_TOKEN }}
46
+
47
+ build-android:
48
+ runs-on: ubuntu-latest
49
+ needs: test
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - uses: actions/setup-node@v4
54
+ with:
55
+ node-version: '20'
56
+ cache: 'yarn'
57
+
58
+ - uses: actions/setup-java@v4
59
+ with:
60
+ distribution: 'temurin'
61
+ java-version: '17'
62
+
63
+ - name: Cache Gradle
64
+ uses: actions/cache@v4
65
+ with:
66
+ path: |
67
+ ~/.gradle/caches
68
+ ~/.gradle/wrapper
69
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
70
+
71
+ - name: Install dependencies
72
+ run: yarn install --frozen-lockfile
73
+
74
+ - name: Build Android APK (debug)
75
+ run: cd android && ./gradlew assembleDebug
76
+
77
+ - name: Upload APK
78
+ uses: actions/upload-artifact@v4
79
+ with:
80
+ name: app-debug.apk
81
+ path: android/app/build/outputs/apk/debug/app-debug.apk
82
+
83
+ build-ios:
84
+ runs-on: macos-15
85
+ needs: test
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+
89
+ - uses: actions/setup-node@v4
90
+ with:
91
+ node-version: '20'
92
+ cache: 'yarn'
93
+
94
+ - name: Cache CocoaPods
95
+ uses: actions/cache@v4
96
+ with:
97
+ path: ios/Pods
98
+ key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
99
+
100
+ - name: Install dependencies
101
+ run: yarn install --frozen-lockfile && cd ios && pod install
102
+
103
+ - name: Build iOS (simulator)
104
+ run: |
105
+ xcodebuild -workspace ios/MyApp.xcworkspace \
106
+ -scheme MyApp \
107
+ -sdk iphonesimulator \
108
+ -configuration Debug \
109
+ -derivedDataPath ios/build \
110
+ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
111
+ ```
112
+
113
+ ---
114
+
115
+ ## React Native — E2E with Detox
116
+
117
+ ```yaml
118
+ # .github/workflows/rn-e2e.yml
119
+ name: E2E Tests (Detox)
120
+
121
+ on:
122
+ push:
123
+ branches: [main]
124
+
125
+ jobs:
126
+ e2e-ios:
127
+ runs-on: macos-15
128
+ steps:
129
+ - uses: actions/checkout@v4
130
+
131
+ - uses: actions/setup-node@v4
132
+ with:
133
+ node-version: '20'
134
+ cache: 'yarn'
135
+
136
+ - name: Install dependencies
137
+ run: yarn install --frozen-lockfile && cd ios && pod install
138
+
139
+ - name: Install Detox CLI
140
+ run: npm install -g detox-cli
141
+
142
+ - name: Build for Detox
143
+ run: detox build --configuration ios.sim.debug
144
+
145
+ - name: Run E2E tests
146
+ run: detox test --configuration ios.sim.debug --headless
147
+
148
+ - name: Upload Detox artifacts on failure
149
+ if: failure()
150
+ uses: actions/upload-artifact@v4
151
+ with:
152
+ name: detox-artifacts
153
+ path: artifacts/
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Flutter — CI Pipeline
159
+
160
+ ```yaml
161
+ # .github/workflows/flutter-ci.yml
162
+ name: Flutter CI
163
+
164
+ on:
165
+ push:
166
+ branches: [main, develop]
167
+ pull_request:
168
+ branches: [main]
169
+
170
+ jobs:
171
+ test:
172
+ runs-on: ubuntu-latest
173
+ steps:
174
+ - uses: actions/checkout@v4
175
+
176
+ - uses: subosito/flutter-action@v2
177
+ with:
178
+ flutter-version: '3.27.x'
179
+ cache: true
180
+
181
+ - name: Install dependencies
182
+ run: flutter pub get
183
+
184
+ - name: Analyze
185
+ run: flutter analyze
186
+
187
+ - name: Unit + Widget tests
188
+ run: flutter test --coverage
189
+
190
+ - name: Upload coverage
191
+ uses: codecov/codecov-action@v4
192
+
193
+ build-android:
194
+ runs-on: ubuntu-latest
195
+ needs: test
196
+ steps:
197
+ - uses: actions/checkout@v4
198
+
199
+ - uses: actions/setup-java@v4
200
+ with:
201
+ distribution: 'temurin'
202
+ java-version: '17'
203
+
204
+ - uses: subosito/flutter-action@v2
205
+ with:
206
+ flutter-version: '3.27.x'
207
+ cache: true
208
+
209
+ - name: Install dependencies
210
+ run: flutter pub get
211
+
212
+ - name: Build APK
213
+ run: flutter build apk --debug
214
+
215
+ - name: Upload APK
216
+ uses: actions/upload-artifact@v4
217
+ with:
218
+ name: flutter-debug.apk
219
+ path: build/app/outputs/flutter-apk/app-debug.apk
220
+
221
+ build-ios:
222
+ runs-on: macos-15
223
+ needs: test
224
+ steps:
225
+ - uses: actions/checkout@v4
226
+
227
+ - uses: subosito/flutter-action@v2
228
+ with:
229
+ flutter-version: '3.27.x'
230
+ cache: true
231
+
232
+ - name: Install dependencies
233
+ run: flutter pub get
234
+
235
+ - name: Build iOS (no codesign)
236
+ run: flutter build ios --debug --no-codesign
237
+ ```
238
+
239
+ ---
240
+
241
+ ## iOS — Release to TestFlight (Fastlane)
242
+
243
+ ```yaml
244
+ # .github/workflows/ios-release.yml
245
+ name: iOS Release
246
+
247
+ on:
248
+ push:
249
+ tags: ['v*']
250
+
251
+ jobs:
252
+ release:
253
+ runs-on: macos-15
254
+ steps:
255
+ - uses: actions/checkout@v4
256
+
257
+ - uses: actions/setup-node@v4
258
+ with:
259
+ node-version: '20'
260
+ cache: 'yarn'
261
+
262
+ - name: Install dependencies
263
+ run: yarn install --frozen-lockfile && cd ios && pod install
264
+
265
+ - name: Setup certificates
266
+ uses: apple-actions/import-codesign-certs@v2
267
+ with:
268
+ p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
269
+ p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
270
+
271
+ - name: Setup provisioning profile
272
+ uses: apple-actions/download-provisioning-profiles@v1
273
+ with:
274
+ bundle-id: com.myapp
275
+ issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
276
+ api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
277
+ api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
278
+
279
+ - name: Deploy to TestFlight
280
+ run: bundle exec fastlane ios beta
281
+ env:
282
+ APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APPSTORE_KEY_ID }}
283
+ APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APPSTORE_ISSUER_ID }}
284
+ APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APPSTORE_PRIVATE_KEY }}
285
+ ```
286
+
287
+ ```ruby
288
+ # ios/Fastfile
289
+ lane :beta do
290
+ build_app(
291
+ workspace: "MyApp.xcworkspace",
292
+ scheme: "MyApp",
293
+ configuration: "Release",
294
+ export_method: "app-store"
295
+ )
296
+ upload_to_testflight(skip_waiting_for_build_processing: true)
297
+ end
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Android — Release to Play Store (Fastlane)
303
+
304
+ ```yaml
305
+ # .github/workflows/android-release.yml
306
+ name: Android Release
307
+
308
+ on:
309
+ push:
310
+ tags: ['v*']
311
+
312
+ jobs:
313
+ release:
314
+ runs-on: ubuntu-latest
315
+ steps:
316
+ - uses: actions/checkout@v4
317
+
318
+ - uses: actions/setup-java@v4
319
+ with:
320
+ distribution: 'temurin'
321
+ java-version: '17'
322
+
323
+ - uses: actions/setup-node@v4
324
+ with:
325
+ node-version: '20'
326
+ cache: 'yarn'
327
+
328
+ - name: Install dependencies
329
+ run: yarn install --frozen-lockfile
330
+
331
+ - name: Decode keystore
332
+ run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
333
+
334
+ - name: Build release AAB
335
+ run: cd android && ./gradlew bundleRelease
336
+ env:
337
+ KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
338
+ KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
339
+ KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
340
+
341
+ - name: Deploy to Play Store
342
+ uses: r0adkll/upload-google-play@v1
343
+ with:
344
+ serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
345
+ packageName: com.myapp
346
+ releaseFiles: android/app/build/outputs/bundle/release/*.aab
347
+ track: internal
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Required GitHub Secrets
353
+
354
+ ```
355
+ # iOS
356
+ CERTIFICATES_P12 ← base64-encoded .p12 file
357
+ CERTIFICATES_P12_PASSWORD ← password for .p12
358
+ APPSTORE_KEY_ID ← App Store Connect API key ID
359
+ APPSTORE_ISSUER_ID ← App Store Connect issuer ID
360
+ APPSTORE_PRIVATE_KEY ← App Store Connect private key (.p8 contents)
361
+
362
+ # Android
363
+ KEYSTORE_BASE64 ← base64-encoded release.keystore
364
+ KEYSTORE_PASSWORD ← keystore password
365
+ KEY_ALIAS ← key alias
366
+ KEY_PASSWORD ← key password
367
+ PLAY_STORE_SERVICE_ACCOUNT_JSON ← GCP service account JSON
368
+
369
+ # Shared
370
+ CODECOV_TOKEN ← coverage reporting
371
+ MAESTRO_API_KEY ← Maestro Cloud E2E (optional)
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Caching Strategy
377
+
378
+ ```yaml
379
+ # Node modules — hash package-lock or yarn.lock
380
+ - uses: actions/cache@v4
381
+ with:
382
+ path: node_modules
383
+ key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
384
+ restore-keys: ${{ runner.os }}-node-
385
+
386
+ # Gradle — hash .gradle files
387
+ - uses: actions/cache@v4
388
+ with:
389
+ path: |
390
+ ~/.gradle/caches
391
+ ~/.gradle/wrapper
392
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
393
+
394
+ # CocoaPods — hash Podfile.lock
395
+ - uses: actions/cache@v4
396
+ with:
397
+ path: ios/Pods
398
+ key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}
399
+
400
+ # Flutter pub — hash pubspec.lock
401
+ - uses: actions/cache@v4
402
+ with:
403
+ path: ~/.pub-cache
404
+ key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
405
+ ```
406
+
407
+ ---
408
+
409
+ ## Anti-Patterns
410
+
411
+ ```
412
+ ❌ Committing signing credentials to repo
413
+ ❌ Running E2E on every PR (too slow — run on main only)
414
+ ❌ No caching (3x slower builds)
415
+ ❌ Skipping unit tests before build jobs
416
+ ❌ Building on push to every branch
417
+
418
+ ✅ Store all secrets in GitHub Secrets
419
+ ✅ Cache node_modules + Gradle + CocoaPods + pub-cache
420
+ ✅ Unit tests on PR, E2E on merge to main
421
+ ✅ needs: test before build jobs
422
+ ✅ Upload build artifacts for download/QA
423
+ ```
@@ -0,0 +1,332 @@
1
+ # Mobile Testing Strategy — Unit + E2E
2
+
3
+ > Test the right things at the right layer. Don't test implementation details.
4
+
5
+ ---
6
+
7
+ ## Testing Pyramid
8
+
9
+ ```
10
+ ╱ E2E ╲ ← Detox / Maestro / XCUITest / Espresso
11
+ ╱───────╲ ← Few, slow, high-confidence
12
+ ╱Integration╲ ← API mocking, navigation flows
13
+ ╱─────────────╲ ← Medium count
14
+ ╱ Unit Tests ╲ ← Jest / XCTest / JUnit
15
+ ╱─────────────────╲ ← Many, fast, cheap
16
+
17
+ RULE: Most tests = unit. E2E = critical flows only (login, checkout, onboarding).
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Unit Tests (Jest — React Native / TypeScript)
23
+
24
+ ```typescript
25
+ // Test hooks, not components
26
+ describe('useCart', () => {
27
+ it('adds item and updates total', () => {
28
+ const { result } = renderHook(() => useCart(), { wrapper: ReduxProvider })
29
+ act(() => { result.current.addItem(mockProduct) })
30
+ expect(result.current.total).toBe(mockProduct.price)
31
+ })
32
+
33
+ it('handles addToCart API error with rollback', async () => {
34
+ server.use(rest.post('/cart', (req, res, ctx) => res(ctx.status(500))))
35
+ const { result } = renderHook(() => useCart(), { wrapper: ReduxProvider })
36
+ await act(async () => { await result.current.addItem(mockProduct) })
37
+ expect(result.current.items).toHaveLength(0) // rolled back
38
+ expect(result.current.error).toBeTruthy()
39
+ })
40
+ })
41
+
42
+ // Test Redux slices directly
43
+ describe('cartSlice', () => {
44
+ it('sets loading state on fetchCart.pending', () => {
45
+ const state = cartReducer(initialState, fetchCart.pending('', undefined))
46
+ expect(state.status).toBe('loading')
47
+ })
48
+ })
49
+ ```
50
+
51
+ **Rules:**
52
+ - Test business logic (hooks, services, slices) — NOT component layout
53
+ - Mock API calls with `msw` (Mock Service Worker)
54
+ - 4 states per feature: loading / success / error / empty
55
+
56
+ ---
57
+
58
+ ## E2E Testing — Detox (React Native)
59
+
60
+ > Best for: React Native apps, full native bridge testing.
61
+
62
+ ### Setup
63
+
64
+ ```bash
65
+ # Install
66
+ npm install --save-dev detox @config/detox
67
+
68
+ # iOS build (required before tests)
69
+ detox build --configuration ios.sim.debug
70
+
71
+ # Run tests
72
+ detox test --configuration ios.sim.debug
73
+ detox test --configuration android.emu.debug
74
+ ```
75
+
76
+ ### Config (`detox.config.js`)
77
+
78
+ ```js
79
+ module.exports = {
80
+ testRunner: {
81
+ args: { '$0': 'jest', config: 'e2e/jest.config.js' },
82
+ jest: { setupTimeout: 120000 }
83
+ },
84
+ apps: {
85
+ 'ios.debug': { type: 'ios.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app', build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build' },
86
+ 'android.debug': { type: 'android.apk', binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug' }
87
+ },
88
+ devices: {
89
+ simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
90
+ emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } }
91
+ },
92
+ configurations: {
93
+ 'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
94
+ 'android.emu.debug': { device: 'emulator', app: 'android.debug' }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Write Detox Tests
100
+
101
+ ```typescript
102
+ // e2e/login.test.ts
103
+ describe('Login Flow', () => {
104
+ beforeAll(async () => {
105
+ await device.launchApp({ newInstance: true })
106
+ })
107
+
108
+ beforeEach(async () => {
109
+ await device.reloadReactNative()
110
+ })
111
+
112
+ it('shows error on wrong password', async () => {
113
+ await element(by.id('email-input')).typeText('user@test.com')
114
+ await element(by.id('password-input')).typeText('wrongpass')
115
+ await element(by.id('login-button')).tap()
116
+ await expect(element(by.text('Invalid credentials'))).toBeVisible()
117
+ })
118
+
119
+ it('navigates to home on success', async () => {
120
+ await element(by.id('email-input')).typeText('user@test.com')
121
+ await element(by.id('password-input')).typeText('correctpass')
122
+ await element(by.id('login-button')).tap()
123
+ await expect(element(by.id('home-screen'))).toBeVisible()
124
+ })
125
+ })
126
+ ```
127
+
128
+ **testID rules:**
129
+ ```typescript
130
+ // Add testID to interactive elements
131
+ <TextInput testID="email-input" ... />
132
+ <TouchableOpacity testID="login-button" ... />
133
+ <View testID="home-screen" ... />
134
+ ```
135
+
136
+ **What to test with Detox:**
137
+ - Login / logout flow
138
+ - Onboarding (first-time user)
139
+ - Critical purchase / checkout path
140
+ - Push notification tap → navigation
141
+ - Deep link handling
142
+
143
+ **What NOT to test with Detox:** minor UI variations, loading spinners, animations.
144
+
145
+ ---
146
+
147
+ ## E2E Testing — Maestro (Cross-Platform)
148
+
149
+ > Best for: Simpler setup, works on React Native + Flutter + native iOS/Android.
150
+
151
+ ### Setup
152
+
153
+ ```bash
154
+ # macOS
155
+ brew tap mobile-dev-inc/tap
156
+ brew install maestro
157
+
158
+ # Run a flow
159
+ maestro test e2e/login.yaml
160
+ maestro test e2e/ # all flows in folder
161
+ ```
162
+
163
+ ### Write Maestro Flows (YAML)
164
+
165
+ ```yaml
166
+ # e2e/login.yaml
167
+ appId: com.myapp
168
+ ---
169
+ - launchApp:
170
+ clearState: true
171
+
172
+ - assertVisible: "Sign In"
173
+
174
+ - tapOn:
175
+ id: "email-input"
176
+ - inputText: "user@test.com"
177
+
178
+ - tapOn:
179
+ id: "password-input"
180
+ - inputText: "wrongpass"
181
+
182
+ - tapOn:
183
+ id: "login-button"
184
+
185
+ - assertVisible: "Invalid credentials"
186
+ ```
187
+
188
+ ```yaml
189
+ # e2e/checkout.yaml
190
+ appId: com.myapp
191
+ ---
192
+ - launchApp
193
+ - tapOn: "Products"
194
+ - tapOn:
195
+ index: 0 # first product
196
+ - tapOn: "Add to Cart"
197
+ - tapOn: "Checkout"
198
+ - assertVisible: "Order Confirmed"
199
+ ```
200
+
201
+ ### Maestro Cloud CI
202
+
203
+ ```bash
204
+ # Run on real devices in Maestro Cloud
205
+ maestro cloud --apiKey $MAESTRO_API_KEY e2e/
206
+ ```
207
+
208
+ **Maestro vs Detox:**
209
+
210
+ | | Maestro | Detox |
211
+ |--|---------|-------|
212
+ | Setup | Minutes | Hours |
213
+ | YAML / Code | YAML | TypeScript |
214
+ | Cross-platform | ✅ RN + Flutter + native | RN only |
215
+ | Speed | Slower | Faster |
216
+ | Power | Medium | High |
217
+ | CI integration | Maestro Cloud | Self-hosted |
218
+
219
+ **Use Maestro when:** simple flows, cross-platform team, quick setup.
220
+ **Use Detox when:** complex interactions, React Native only, full control.
221
+
222
+ ---
223
+
224
+ ## Flutter Testing
225
+
226
+ ### Unit + Widget Tests
227
+
228
+ ```dart
229
+ // Unit test — business logic
230
+ test('CartBloc adds item correctly', () {
231
+ final bloc = CartBloc(cartRepository: MockCartRepository());
232
+ bloc.add(AddToCart(product: mockProduct));
233
+ expectLater(bloc.stream, emits(CartLoaded(items: [mockProduct])));
234
+ });
235
+
236
+ // Widget test — UI
237
+ testWidgets('ProductCard shows title and price', (tester) async {
238
+ await tester.pumpWidget(MaterialApp(
239
+ home: ProductCard(product: mockProduct),
240
+ ));
241
+ expect(find.text(mockProduct.title), findsOneWidget);
242
+ expect(find.text('\$${mockProduct.price}'), findsOneWidget);
243
+ });
244
+ ```
245
+
246
+ ### Integration Test (Flutter Driver replacement)
247
+
248
+ ```dart
249
+ // integration_test/login_test.dart
250
+ void main() {
251
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
252
+
253
+ testWidgets('login flow', (tester) async {
254
+ app.main();
255
+ await tester.pumpAndSettle();
256
+
257
+ await tester.enterText(find.byKey(Key('email')), 'user@test.com');
258
+ await tester.enterText(find.byKey(Key('password')), 'pass123');
259
+ await tester.tap(find.byKey(Key('login-button')));
260
+ await tester.pumpAndSettle();
261
+
262
+ expect(find.byKey(Key('home-screen')), findsOneWidget);
263
+ });
264
+ }
265
+ ```
266
+
267
+ ```bash
268
+ # Run on simulator
269
+ flutter test integration_test/
270
+
271
+ # Run on real device
272
+ flutter test integration_test/ -d <device-id>
273
+ ```
274
+
275
+ ---
276
+
277
+ ## iOS — XCUITest
278
+
279
+ ```swift
280
+ func testLoginFlow() throws {
281
+ let app = XCUIApplication()
282
+ app.launch()
283
+
284
+ let emailField = app.textFields["email-input"]
285
+ emailField.tap()
286
+ emailField.typeText("user@test.com")
287
+
288
+ let passwordField = app.secureTextFields["password-input"]
289
+ passwordField.tap()
290
+ passwordField.typeText("pass123")
291
+
292
+ app.buttons["login-button"].tap()
293
+
294
+ XCTAssertTrue(app.otherElements["home-screen"].waitForExistence(timeout: 5))
295
+ }
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Android — Espresso
301
+
302
+ ```kotlin
303
+ @Test
304
+ fun testLoginFlow() {
305
+ onView(withId(R.id.emailInput))
306
+ .perform(typeText("user@test.com"), closeSoftKeyboard())
307
+ onView(withId(R.id.passwordInput))
308
+ .perform(typeText("pass123"), closeSoftKeyboard())
309
+ onView(withId(R.id.loginButton)).perform(click())
310
+ onView(withId(R.id.homeScreen)).check(matches(isDisplayed()))
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Anti-Patterns
317
+
318
+ ```
319
+ ❌ Testing implementation details (internal state, private methods)
320
+ ❌ Testing every UI pixel (visual regression belongs in Storybook/Percy)
321
+ ❌ E2E for every edge case (unit test those)
322
+ ❌ Skipping testID on interactive elements
323
+ ❌ Running Detox without a stable test build
324
+ ❌ Using sleep() instead of waitFor()
325
+
326
+ ✅ Test user flows, not code internals
327
+ ✅ E2E for critical paths: login, purchase, onboarding
328
+ ✅ Unit test all business logic (hooks, slices, services)
329
+ ✅ Mock API calls in unit tests
330
+ ✅ Add testID to every tappable + input element
331
+ ✅ waitFor() over sleep() in Detox
332
+ ```