@buivietphi/skill-mobile-mt 1.4.1 → 1.4.2

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/AGENTS.md CHANGED
@@ -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)
@@ -70,7 +72,7 @@ skill-mobile-mt/
70
72
 
71
73
  **Token totals:**
72
74
  - 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)
75
+ - Full load (all files): **~74,700 tokens** (58.4% 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
 
@@ -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
217
+ max_tokens: 74700
214
218
  smart_load_tokens: 38600
215
- savings: "~45%"
219
+ savings: "~48%"
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
  ---
package/README.md CHANGED
@@ -232,7 +232,7 @@ iOS only?
232
232
  | SKILL.md only | ~13,200 | 10.3% | 6.6% |
233
233
  | + 1 platform + core shared/ | ~38,600 | 30.2% | 19.3% |
234
234
  | Cross-platform (RN/Flutter + iOS + Android) | ~53,000 | 41.4% | 26.5% |
235
- | All files loaded | ~70,000 | 54.7% | 35.0% |
235
+ | All files loaded | ~74,700 | 58.4% | 37.4% |
236
236
  | **Smart load (recommended)** | **~38,600** | **30.2%** | **19.3%** |
237
237
 
238
238
  ### Per-file token breakdown
@@ -259,9 +259,11 @@ iOS only?
259
259
  | `shared/version-management.md` | 3,500 |
260
260
  | `shared/observability.md` | 3,000 |
261
261
  | `shared/offline-first.md` | 2,566 |
262
+ | `shared/testing-strategy.md` | 2,200 |
263
+ | `shared/ci-cd.md` | 2,500 |
262
264
  | `shared/claude-md-template.md` | ~500 |
263
265
  | `shared/agent-rules-template.md` | ~2,500 |
264
- | **Total** | **~48,800** |
266
+ | **Total** | **~53,500** |
265
267
 
266
268
  ## Installed Structure
267
269
 
@@ -293,6 +295,8 @@ iOS only?
293
295
  ├── observability.md Sessions as 4th pillar
294
296
  ├── release-checklist.md Pre-release verification
295
297
  ├── offline-first.md Local-first + sync patterns
298
+ ├── testing-strategy.md Detox + Maestro + XCUITest + Espresso E2E
299
+ ├── ci-cd.md GitHub Actions CI/CD templates
296
300
  ├── claude-md-template.md CLAUDE.md template for projects
297
301
  └── agent-rules-template.md Rules templates for all agents
298
302
  ```
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.2"
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.**
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.2 │`);
74
74
  log(` │ Master Senior Mobile Engineer │`);
75
75
  log(` │ │`);
76
76
  log(` │ Claude · Cline · Roo Code · Cursor · Windsurf │`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buivietphi/skill-mobile-mt",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
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.",
5
5
  "author": "buivietphi",
6
6
  "license": "MIT",
@@ -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
+ ```