@buivietphi/skill-mobile-mt 2.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/AGENTS.md +482 -0
- package/README.md +528 -0
- package/SKILL.md +1399 -0
- package/android/android-native.md +480 -0
- package/bin/install.mjs +976 -0
- package/flutter/flutter.md +304 -0
- package/humanizer/humanizer-mobile.md +295 -0
- package/ios/ios-native.md +182 -0
- package/package.json +56 -0
- package/react-native/react-native.md +743 -0
- package/shared/agent-rules-template.md +343 -0
- package/shared/ai-dlc-workflow.md +237 -0
- package/shared/anti-patterns.md +407 -0
- package/shared/architecture-intelligence.md +416 -0
- package/shared/bug-detection.md +71 -0
- package/shared/ci-cd.md +423 -0
- package/shared/claude-md-template.md +125 -0
- package/shared/code-review.md +133 -0
- package/shared/common-pitfalls.md +117 -0
- package/shared/document-analysis.md +167 -0
- package/shared/error-recovery.md +467 -0
- package/shared/observability.md +688 -0
- package/shared/offline-first.md +377 -0
- package/shared/performance-prediction.md +210 -0
- package/shared/platform-excellence.md +244 -0
- package/shared/prompt-engineering.md +705 -0
- package/shared/release-checklist.md +82 -0
- package/shared/testing-strategy.md +332 -0
- package/shared/ui-ux-mobile.md +667 -0
- package/shared/version-management.md +526 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Release Checklist — Before Shipping
|
|
2
|
+
|
|
3
|
+
> Complete ALL items before submitting to App Store / Play Store.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Code Quality
|
|
8
|
+
|
|
9
|
+
- [ ] No `console.log` / `print` / `NSLog` in production
|
|
10
|
+
- [ ] No hardcoded secrets or API keys
|
|
11
|
+
- [ ] No force unwraps without safety (`!` / `!!` / `as!`)
|
|
12
|
+
- [ ] No empty catch blocks
|
|
13
|
+
- [ ] No files > 500 lines
|
|
14
|
+
- [ ] All TODO/FIXME resolved or tracked
|
|
15
|
+
|
|
16
|
+
## Security
|
|
17
|
+
|
|
18
|
+
- [ ] Tokens stored in SecureStore / Keychain / EncryptedSharedPreferences
|
|
19
|
+
- [ ] Deep links validated before navigation
|
|
20
|
+
- [ ] No sensitive data in logs
|
|
21
|
+
- [ ] SSL pinning enabled (if required)
|
|
22
|
+
- [ ] ProGuard / R8 enabled for Android release
|
|
23
|
+
- [ ] Debug mode stripped from release build
|
|
24
|
+
|
|
25
|
+
## Performance
|
|
26
|
+
|
|
27
|
+
- [ ] Lists use FlatList / ListView.builder / LazyColumn (not ScrollView)
|
|
28
|
+
- [ ] Images optimized (resized, cached, lazy loaded)
|
|
29
|
+
- [ ] No main thread blocking
|
|
30
|
+
- [ ] No memory leaks (useEffect cleanup, dispose, [weak self])
|
|
31
|
+
- [ ] App launch time < 3 seconds
|
|
32
|
+
- [ ] Smooth scrolling (60fps)
|
|
33
|
+
|
|
34
|
+
## UI / UX
|
|
35
|
+
|
|
36
|
+
- [ ] All states handled: loading, success, error, empty
|
|
37
|
+
- [ ] Touch targets >= 44pt (iOS) / 48dp (Android)
|
|
38
|
+
- [ ] Safe area / notch handled
|
|
39
|
+
- [ ] Keyboard dismissal handled
|
|
40
|
+
- [ ] Back button works (Android)
|
|
41
|
+
- [ ] Accessibility labels on interactive elements
|
|
42
|
+
- [ ] Dark mode works (if supported)
|
|
43
|
+
|
|
44
|
+
## Platform-Specific
|
|
45
|
+
|
|
46
|
+
### iOS
|
|
47
|
+
- [ ] Correct deployment target set
|
|
48
|
+
- [ ] App icons all sizes provided
|
|
49
|
+
- [ ] Privacy usage descriptions in Info.plist
|
|
50
|
+
- [ ] `pod install` runs clean
|
|
51
|
+
- [ ] Tested on physical device
|
|
52
|
+
|
|
53
|
+
### Android
|
|
54
|
+
- [ ] `minSdk` / `targetSdk` correct
|
|
55
|
+
- [ ] Signing config for release
|
|
56
|
+
- [ ] Permissions declared in AndroidManifest.xml
|
|
57
|
+
- [ ] Tested on physical device
|
|
58
|
+
- [ ] 64-bit support included
|
|
59
|
+
|
|
60
|
+
### React Native / Flutter
|
|
61
|
+
- [ ] Both iOS + Android tested
|
|
62
|
+
- [ ] Native modules compile on both platforms
|
|
63
|
+
- [ ] Bundle size acceptable
|
|
64
|
+
- [ ] Hermes enabled (RN) / tree-shaking (Flutter)
|
|
65
|
+
|
|
66
|
+
## Testing
|
|
67
|
+
|
|
68
|
+
- [ ] Critical paths tested manually
|
|
69
|
+
- [ ] Edge cases: offline, slow network, empty data, expired token
|
|
70
|
+
- [ ] Fresh install tested (no cached data)
|
|
71
|
+
- [ ] Upgrade from previous version tested (if applicable)
|
|
72
|
+
- [ ] Crash reporting SDK integrated (Sentry / Firebase Crashlytics)
|
|
73
|
+
|
|
74
|
+
## Final
|
|
75
|
+
|
|
76
|
+
- [ ] Version number bumped
|
|
77
|
+
- [ ] Changelog updated
|
|
78
|
+
- [ ] Screenshots / store listing updated (if UI changed)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
> Ship when ALL boxes are checked. Not before.
|
|
@@ -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
|
+
```
|