agentic-team-templates 0.3.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/README.md +280 -0
- package/bin/cli.js +5 -0
- package/package.json +47 -0
- package/src/index.js +521 -0
- package/templates/_shared/code-quality.md +162 -0
- package/templates/_shared/communication.md +114 -0
- package/templates/_shared/core-principles.md +62 -0
- package/templates/_shared/git-workflow.md +165 -0
- package/templates/_shared/security-fundamentals.md +173 -0
- package/templates/blockchain/.cursorrules/defi-patterns.md +520 -0
- package/templates/blockchain/.cursorrules/gas-optimization.md +339 -0
- package/templates/blockchain/.cursorrules/overview.md +130 -0
- package/templates/blockchain/.cursorrules/security.md +318 -0
- package/templates/blockchain/.cursorrules/smart-contracts.md +364 -0
- package/templates/blockchain/.cursorrules/testing.md +415 -0
- package/templates/blockchain/.cursorrules/web3-integration.md +538 -0
- package/templates/blockchain/CLAUDE.md +389 -0
- package/templates/cli-tools/.cursorrules/architecture.md +412 -0
- package/templates/cli-tools/.cursorrules/arguments.md +406 -0
- package/templates/cli-tools/.cursorrules/distribution.md +546 -0
- package/templates/cli-tools/.cursorrules/error-handling.md +455 -0
- package/templates/cli-tools/.cursorrules/overview.md +136 -0
- package/templates/cli-tools/.cursorrules/testing.md +537 -0
- package/templates/cli-tools/.cursorrules/user-experience.md +545 -0
- package/templates/cli-tools/CLAUDE.md +356 -0
- package/templates/data-engineering/.cursorrules/data-modeling.md +367 -0
- package/templates/data-engineering/.cursorrules/data-quality.md +455 -0
- package/templates/data-engineering/.cursorrules/overview.md +85 -0
- package/templates/data-engineering/.cursorrules/performance.md +339 -0
- package/templates/data-engineering/.cursorrules/pipeline-design.md +280 -0
- package/templates/data-engineering/.cursorrules/security.md +460 -0
- package/templates/data-engineering/.cursorrules/testing.md +452 -0
- package/templates/data-engineering/CLAUDE.md +974 -0
- package/templates/devops-sre/.cursorrules/capacity-planning.md +653 -0
- package/templates/devops-sre/.cursorrules/change-management.md +584 -0
- package/templates/devops-sre/.cursorrules/chaos-engineering.md +651 -0
- package/templates/devops-sre/.cursorrules/disaster-recovery.md +641 -0
- package/templates/devops-sre/.cursorrules/incident-management.md +565 -0
- package/templates/devops-sre/.cursorrules/observability.md +714 -0
- package/templates/devops-sre/.cursorrules/overview.md +230 -0
- package/templates/devops-sre/.cursorrules/postmortems.md +588 -0
- package/templates/devops-sre/.cursorrules/runbooks.md +760 -0
- package/templates/devops-sre/.cursorrules/slo-sli.md +617 -0
- package/templates/devops-sre/.cursorrules/toil-reduction.md +567 -0
- package/templates/devops-sre/CLAUDE.md +1007 -0
- package/templates/documentation/.cursorrules/adr.md +277 -0
- package/templates/documentation/.cursorrules/api-documentation.md +411 -0
- package/templates/documentation/.cursorrules/code-comments.md +253 -0
- package/templates/documentation/.cursorrules/maintenance.md +260 -0
- package/templates/documentation/.cursorrules/overview.md +82 -0
- package/templates/documentation/.cursorrules/readme-standards.md +306 -0
- package/templates/documentation/CLAUDE.md +120 -0
- package/templates/fullstack/.cursorrules/api-contracts.md +331 -0
- package/templates/fullstack/.cursorrules/architecture.md +298 -0
- package/templates/fullstack/.cursorrules/overview.md +109 -0
- package/templates/fullstack/.cursorrules/shared-types.md +348 -0
- package/templates/fullstack/.cursorrules/testing.md +386 -0
- package/templates/fullstack/CLAUDE.md +349 -0
- package/templates/ml-ai/.cursorrules/data-engineering.md +483 -0
- package/templates/ml-ai/.cursorrules/deployment.md +601 -0
- package/templates/ml-ai/.cursorrules/model-development.md +538 -0
- package/templates/ml-ai/.cursorrules/monitoring.md +658 -0
- package/templates/ml-ai/.cursorrules/overview.md +131 -0
- package/templates/ml-ai/.cursorrules/security.md +637 -0
- package/templates/ml-ai/.cursorrules/testing.md +678 -0
- package/templates/ml-ai/CLAUDE.md +1136 -0
- package/templates/mobile/.cursorrules/navigation.md +246 -0
- package/templates/mobile/.cursorrules/offline-first.md +302 -0
- package/templates/mobile/.cursorrules/overview.md +71 -0
- package/templates/mobile/.cursorrules/performance.md +345 -0
- package/templates/mobile/.cursorrules/testing.md +339 -0
- package/templates/mobile/CLAUDE.md +233 -0
- package/templates/platform-engineering/.cursorrules/ci-cd.md +778 -0
- package/templates/platform-engineering/.cursorrules/developer-experience.md +632 -0
- package/templates/platform-engineering/.cursorrules/infrastructure-as-code.md +600 -0
- package/templates/platform-engineering/.cursorrules/kubernetes.md +710 -0
- package/templates/platform-engineering/.cursorrules/observability.md +747 -0
- package/templates/platform-engineering/.cursorrules/overview.md +215 -0
- package/templates/platform-engineering/.cursorrules/security.md +855 -0
- package/templates/platform-engineering/.cursorrules/testing.md +878 -0
- package/templates/platform-engineering/CLAUDE.md +850 -0
- package/templates/utility-agent/.cursorrules/action-control.md +284 -0
- package/templates/utility-agent/.cursorrules/context-management.md +186 -0
- package/templates/utility-agent/.cursorrules/hallucination-prevention.md +253 -0
- package/templates/utility-agent/.cursorrules/overview.md +78 -0
- package/templates/utility-agent/.cursorrules/token-optimization.md +369 -0
- package/templates/utility-agent/CLAUDE.md +513 -0
- package/templates/web-backend/.cursorrules/api-design.md +255 -0
- package/templates/web-backend/.cursorrules/authentication.md +309 -0
- package/templates/web-backend/.cursorrules/database-patterns.md +298 -0
- package/templates/web-backend/.cursorrules/error-handling.md +366 -0
- package/templates/web-backend/.cursorrules/overview.md +69 -0
- package/templates/web-backend/.cursorrules/security.md +358 -0
- package/templates/web-backend/.cursorrules/testing.md +395 -0
- package/templates/web-backend/CLAUDE.md +366 -0
- package/templates/web-frontend/.cursorrules/accessibility.md +296 -0
- package/templates/web-frontend/.cursorrules/component-patterns.md +204 -0
- package/templates/web-frontend/.cursorrules/overview.md +72 -0
- package/templates/web-frontend/.cursorrules/performance.md +325 -0
- package/templates/web-frontend/.cursorrules/state-management.md +227 -0
- package/templates/web-frontend/.cursorrules/styling.md +271 -0
- package/templates/web-frontend/.cursorrules/testing.md +311 -0
- package/templates/web-frontend/CLAUDE.md +399 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Mobile Performance
|
|
2
|
+
|
|
3
|
+
Optimizing mobile app performance for smooth user experience.
|
|
4
|
+
|
|
5
|
+
## Performance Goals
|
|
6
|
+
|
|
7
|
+
- **60 FPS**: Smooth animations and scrolling
|
|
8
|
+
- **< 3s**: Cold start time
|
|
9
|
+
- **Responsive**: Touch feedback < 100ms
|
|
10
|
+
- **Efficient**: Minimal battery drain
|
|
11
|
+
|
|
12
|
+
## List Performance
|
|
13
|
+
|
|
14
|
+
### Virtualization
|
|
15
|
+
|
|
16
|
+
Only render visible items.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// React Native FlatList
|
|
20
|
+
<FlatList
|
|
21
|
+
data={items}
|
|
22
|
+
renderItem={({ item }) => <ItemComponent item={item} />}
|
|
23
|
+
keyExtractor={(item) => item.id}
|
|
24
|
+
// Performance optimizations
|
|
25
|
+
removeClippedSubviews={true}
|
|
26
|
+
maxToRenderPerBatch={10}
|
|
27
|
+
windowSize={5}
|
|
28
|
+
initialNumToRender={10}
|
|
29
|
+
getItemLayout={(data, index) => ({
|
|
30
|
+
length: ITEM_HEIGHT,
|
|
31
|
+
offset: ITEM_HEIGHT * index,
|
|
32
|
+
index,
|
|
33
|
+
})}
|
|
34
|
+
/>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Optimized List Items
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// Memoize list items
|
|
41
|
+
const ItemComponent = memo(({ item }: { item: Item }) => (
|
|
42
|
+
<View style={styles.item}>
|
|
43
|
+
<Text>{item.title}</Text>
|
|
44
|
+
</View>
|
|
45
|
+
));
|
|
46
|
+
|
|
47
|
+
// Memoize callbacks
|
|
48
|
+
const renderItem = useCallback(
|
|
49
|
+
({ item }: { item: Item }) => <ItemComponent item={item} />,
|
|
50
|
+
[]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const keyExtractor = useCallback((item: Item) => item.id, []);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Rendering Optimization
|
|
57
|
+
|
|
58
|
+
### Avoid Unnecessary Re-renders
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// Bad: Inline objects cause re-renders
|
|
62
|
+
<View style={{ padding: 10 }}>
|
|
63
|
+
|
|
64
|
+
// Good: Define styles outside
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
container: { padding: 10 },
|
|
67
|
+
});
|
|
68
|
+
<View style={styles.container}>
|
|
69
|
+
|
|
70
|
+
// Bad: Inline functions cause re-renders
|
|
71
|
+
<Button onPress={() => handlePress(id)} />
|
|
72
|
+
|
|
73
|
+
// Good: Memoize callbacks
|
|
74
|
+
const handlePressCallback = useCallback(() => handlePress(id), [id]);
|
|
75
|
+
<Button onPress={handlePressCallback} />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Memoization
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// Memoize expensive components
|
|
82
|
+
const ExpensiveComponent = memo(({ data }) => {
|
|
83
|
+
// Heavy rendering
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Memoize computed values
|
|
87
|
+
const sortedData = useMemo(
|
|
88
|
+
() => data.slice().sort((a, b) => a.name.localeCompare(b.name)),
|
|
89
|
+
[data]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Memoize callbacks
|
|
93
|
+
const handleSubmit = useCallback((values) => {
|
|
94
|
+
api.submit(values);
|
|
95
|
+
}, []);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Debounce Expensive Operations
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// Debounce search input
|
|
102
|
+
const debouncedSearch = useMemo(
|
|
103
|
+
() => debounce((query: string) => {
|
|
104
|
+
performSearch(query);
|
|
105
|
+
}, 300),
|
|
106
|
+
[]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
<TextInput
|
|
110
|
+
onChangeText={(text) => {
|
|
111
|
+
setQuery(text);
|
|
112
|
+
debouncedSearch(text);
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Image Optimization
|
|
118
|
+
|
|
119
|
+
### Proper Sizing
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// Specify dimensions
|
|
123
|
+
<Image
|
|
124
|
+
source={{ uri: imageUrl }}
|
|
125
|
+
style={{ width: 100, height: 100 }}
|
|
126
|
+
resizeMode="cover"
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
// Use appropriate image sizes from server
|
|
130
|
+
const getImageUrl = (id: string, size: 'thumb' | 'medium' | 'full') => {
|
|
131
|
+
const dimensions = { thumb: 100, medium: 300, full: 1000 };
|
|
132
|
+
return `${API_URL}/images/${id}?w=${dimensions[size]}`;
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Caching
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
// Use FastImage for better caching
|
|
140
|
+
import FastImage from 'react-native-fast-image';
|
|
141
|
+
|
|
142
|
+
<FastImage
|
|
143
|
+
source={{
|
|
144
|
+
uri: imageUrl,
|
|
145
|
+
priority: FastImage.priority.normal,
|
|
146
|
+
cache: FastImage.cacheControl.immutable,
|
|
147
|
+
}}
|
|
148
|
+
style={styles.image}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Lazy Loading
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// Load images as they come into view
|
|
156
|
+
<FlatList
|
|
157
|
+
data={images}
|
|
158
|
+
renderItem={({ item }) => (
|
|
159
|
+
<FastImage
|
|
160
|
+
source={{ uri: item.url }}
|
|
161
|
+
style={styles.image}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
// Images outside window won't load
|
|
165
|
+
windowSize={3}
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Animations
|
|
170
|
+
|
|
171
|
+
### Use Native Driver
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// Animate on native thread
|
|
175
|
+
Animated.timing(animatedValue, {
|
|
176
|
+
toValue: 1,
|
|
177
|
+
duration: 300,
|
|
178
|
+
useNativeDriver: true, // Key for performance
|
|
179
|
+
}).start();
|
|
180
|
+
|
|
181
|
+
// Native driver supports: transform, opacity
|
|
182
|
+
// Does NOT support: width, height, margin, padding, colors
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Use Reanimated for Complex Animations
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import Animated, {
|
|
189
|
+
useSharedValue,
|
|
190
|
+
useAnimatedStyle,
|
|
191
|
+
withSpring,
|
|
192
|
+
} from 'react-native-reanimated';
|
|
193
|
+
|
|
194
|
+
function AnimatedCard() {
|
|
195
|
+
const scale = useSharedValue(1);
|
|
196
|
+
|
|
197
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
198
|
+
transform: [{ scale: scale.value }],
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
const onPressIn = () => {
|
|
202
|
+
scale.value = withSpring(0.95);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const onPressOut = () => {
|
|
206
|
+
scale.value = withSpring(1);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Pressable onPressIn={onPressIn} onPressOut={onPressOut}>
|
|
211
|
+
<Animated.View style={[styles.card, animatedStyle]}>
|
|
212
|
+
{/* Content */}
|
|
213
|
+
</Animated.View>
|
|
214
|
+
</Pressable>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Startup Performance
|
|
220
|
+
|
|
221
|
+
### Defer Non-Critical Work
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
function App() {
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
// Defer analytics, crash reporting setup
|
|
227
|
+
InteractionManager.runAfterInteractions(() => {
|
|
228
|
+
initializeAnalytics();
|
|
229
|
+
initializeCrashReporting();
|
|
230
|
+
});
|
|
231
|
+
}, []);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Lazy Load Screens
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
const SettingsScreen = lazy(() => import('./screens/SettingsScreen'));
|
|
239
|
+
|
|
240
|
+
function AppNavigator() {
|
|
241
|
+
return (
|
|
242
|
+
<Suspense fallback={<ScreenLoader />}>
|
|
243
|
+
<Stack.Screen name="Settings" component={SettingsScreen} />
|
|
244
|
+
</Suspense>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Memory Management
|
|
250
|
+
|
|
251
|
+
### Avoid Memory Leaks
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const subscription = eventEmitter.addListener('event', handler);
|
|
256
|
+
|
|
257
|
+
// Always cleanup
|
|
258
|
+
return () => subscription.remove();
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
let isMounted = true;
|
|
263
|
+
|
|
264
|
+
fetchData().then((data) => {
|
|
265
|
+
if (isMounted) { // Check before setting state
|
|
266
|
+
setData(data);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return () => {
|
|
271
|
+
isMounted = false;
|
|
272
|
+
};
|
|
273
|
+
}, []);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Monitor Memory
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
// React Native Hermes memory usage
|
|
280
|
+
if (global.HermesInternal) {
|
|
281
|
+
const heapInfo = global.HermesInternal.getRuntimeProperties();
|
|
282
|
+
console.log('Heap size:', heapInfo['Heap size']);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Profiling
|
|
287
|
+
|
|
288
|
+
### React DevTools Profiler
|
|
289
|
+
|
|
290
|
+
1. Enable profiler in React DevTools
|
|
291
|
+
2. Record interaction
|
|
292
|
+
3. Analyze component render times
|
|
293
|
+
4. Identify unnecessary re-renders
|
|
294
|
+
|
|
295
|
+
### Performance Monitor
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
// Enable performance monitor in dev
|
|
299
|
+
import { PerformanceMonitor } from 'react-native';
|
|
300
|
+
|
|
301
|
+
// Shows JS/UI frame rates
|
|
302
|
+
PerformanceMonitor.enable();
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Systrace (Android)
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
# Capture trace
|
|
309
|
+
npx react-native profile-hermes
|
|
310
|
+
|
|
311
|
+
# Analyze in Chrome DevTools
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Common Issues
|
|
315
|
+
|
|
316
|
+
### JS Thread Blocking
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
// Bad: Heavy computation on JS thread
|
|
320
|
+
const processedData = heavyComputation(data);
|
|
321
|
+
|
|
322
|
+
// Good: Move to background/worker
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
InteractionManager.runAfterInteractions(async () => {
|
|
325
|
+
const result = await heavyComputation(data);
|
|
326
|
+
setProcessedData(result);
|
|
327
|
+
});
|
|
328
|
+
}, [data]);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Too Many Bridge Calls
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
// Bad: Multiple small updates
|
|
335
|
+
items.forEach((item, i) => {
|
|
336
|
+
Animated.timing(positions[i], { toValue: i * 10 }).start();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Good: Batch updates
|
|
340
|
+
Animated.parallel(
|
|
341
|
+
items.map((item, i) =>
|
|
342
|
+
Animated.timing(positions[i], { toValue: i * 10 })
|
|
343
|
+
)
|
|
344
|
+
).start();
|
|
345
|
+
```
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# Mobile Testing
|
|
2
|
+
|
|
3
|
+
Testing strategies for mobile applications.
|
|
4
|
+
|
|
5
|
+
## Testing Pyramid
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
╱╲ Device/E2E Tests (few, slow)
|
|
9
|
+
╱──╲ Integration Tests
|
|
10
|
+
╱────╲ Component Tests
|
|
11
|
+
╱──────╲ Unit Tests (many, fast)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Unit Tests
|
|
15
|
+
|
|
16
|
+
Test pure functions and business logic.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// utils/formatters.test.ts
|
|
20
|
+
import { formatCurrency, formatDate } from './formatters';
|
|
21
|
+
|
|
22
|
+
describe('formatCurrency', () => {
|
|
23
|
+
it('formats USD correctly', () => {
|
|
24
|
+
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('handles zero', () => {
|
|
28
|
+
expect(formatCurrency(0, 'USD')).toBe('$0.00');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// hooks/useCart.test.ts
|
|
33
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
34
|
+
import { useCart } from './useCart';
|
|
35
|
+
|
|
36
|
+
describe('useCart', () => {
|
|
37
|
+
it('adds item to cart', () => {
|
|
38
|
+
const { result } = renderHook(() => useCart());
|
|
39
|
+
|
|
40
|
+
act(() => {
|
|
41
|
+
result.current.addItem({ id: '1', name: 'Product', price: 10 });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result.current.items).toHaveLength(1);
|
|
45
|
+
expect(result.current.total).toBe(10);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Component Tests
|
|
51
|
+
|
|
52
|
+
Test components in isolation.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// components/Button.test.tsx
|
|
56
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
57
|
+
import { Button } from './Button';
|
|
58
|
+
|
|
59
|
+
describe('Button', () => {
|
|
60
|
+
it('renders label correctly', () => {
|
|
61
|
+
const { getByText } = render(<Button label="Submit" onPress={() => {}} />);
|
|
62
|
+
expect(getByText('Submit')).toBeTruthy();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('calls onPress when pressed', () => {
|
|
66
|
+
const onPress = jest.fn();
|
|
67
|
+
const { getByText } = render(<Button label="Submit" onPress={onPress} />);
|
|
68
|
+
|
|
69
|
+
fireEvent.press(getByText('Submit'));
|
|
70
|
+
|
|
71
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('is disabled when loading', () => {
|
|
75
|
+
const onPress = jest.fn();
|
|
76
|
+
const { getByText } = render(
|
|
77
|
+
<Button label="Submit" onPress={onPress} loading />
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
fireEvent.press(getByText('Submit'));
|
|
81
|
+
|
|
82
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Testing with Async Operations
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// screens/UserProfile.test.tsx
|
|
91
|
+
import { render, waitFor } from '@testing-library/react-native';
|
|
92
|
+
import { UserProfile } from './UserProfile';
|
|
93
|
+
|
|
94
|
+
jest.mock('../services/api');
|
|
95
|
+
|
|
96
|
+
describe('UserProfile', () => {
|
|
97
|
+
it('displays user data after loading', async () => {
|
|
98
|
+
api.getUser.mockResolvedValue({
|
|
99
|
+
id: '1',
|
|
100
|
+
name: 'John Doe',
|
|
101
|
+
email: 'john@example.com',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const { getByText, queryByTestId } = render(<UserProfile userId="1" />);
|
|
105
|
+
|
|
106
|
+
// Loading state
|
|
107
|
+
expect(queryByTestId('loading')).toBeTruthy();
|
|
108
|
+
|
|
109
|
+
// Data loaded
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(getByText('John Doe')).toBeTruthy();
|
|
112
|
+
expect(getByText('john@example.com')).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('shows error on failure', async () => {
|
|
117
|
+
api.getUser.mockRejectedValue(new Error('Network error'));
|
|
118
|
+
|
|
119
|
+
const { getByText } = render(<UserProfile userId="1" />);
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(getByText(/error/i)).toBeTruthy();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Integration Tests
|
|
129
|
+
|
|
130
|
+
Test navigation and screen interactions.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// navigation/AuthFlow.test.tsx
|
|
134
|
+
import { render, fireEvent, waitFor } from '@testing-library/react-native';
|
|
135
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
136
|
+
import { AuthNavigator } from './AuthNavigator';
|
|
137
|
+
|
|
138
|
+
const renderWithNavigation = (component: React.ReactElement) => {
|
|
139
|
+
return render(
|
|
140
|
+
<NavigationContainer>{component}</NavigationContainer>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
describe('Auth Flow', () => {
|
|
145
|
+
it('navigates from login to signup', () => {
|
|
146
|
+
const { getByText } = renderWithNavigation(<AuthNavigator />);
|
|
147
|
+
|
|
148
|
+
fireEvent.press(getByText("Don't have an account? Sign up"));
|
|
149
|
+
|
|
150
|
+
expect(getByText('Create Account')).toBeTruthy();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('logs in and navigates to home', async () => {
|
|
154
|
+
api.login.mockResolvedValue({ token: 'abc', user: { id: '1' } });
|
|
155
|
+
|
|
156
|
+
const { getByPlaceholderText, getByText } = renderWithNavigation(
|
|
157
|
+
<AuthNavigator />
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
fireEvent.changeText(getByPlaceholderText('Email'), 'test@example.com');
|
|
161
|
+
fireEvent.changeText(getByPlaceholderText('Password'), 'password123');
|
|
162
|
+
fireEvent.press(getByText('Log In'));
|
|
163
|
+
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(getByText('Welcome')).toBeTruthy(); // Home screen
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## E2E Tests (Detox)
|
|
172
|
+
|
|
173
|
+
Test full app on device/simulator.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// e2e/login.e2e.ts
|
|
177
|
+
describe('Login Flow', () => {
|
|
178
|
+
beforeAll(async () => {
|
|
179
|
+
await device.launchApp();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
beforeEach(async () => {
|
|
183
|
+
await device.reloadReactNative();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should login successfully', async () => {
|
|
187
|
+
await element(by.id('email-input')).typeText('test@example.com');
|
|
188
|
+
await element(by.id('password-input')).typeText('password123');
|
|
189
|
+
await element(by.id('login-button')).tap();
|
|
190
|
+
|
|
191
|
+
await expect(element(by.id('home-screen'))).toBeVisible();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should show error for invalid credentials', async () => {
|
|
195
|
+
await element(by.id('email-input')).typeText('wrong@example.com');
|
|
196
|
+
await element(by.id('password-input')).typeText('wrongpassword');
|
|
197
|
+
await element(by.id('login-button')).tap();
|
|
198
|
+
|
|
199
|
+
await expect(element(by.text('Invalid credentials'))).toBeVisible();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Detox Best Practices
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Use testID for reliable selectors
|
|
208
|
+
<TouchableOpacity testID="submit-button" onPress={onSubmit}>
|
|
209
|
+
<Text>Submit</Text>
|
|
210
|
+
</TouchableOpacity>
|
|
211
|
+
|
|
212
|
+
// Wait for elements properly
|
|
213
|
+
await waitFor(element(by.id('list-item-0')))
|
|
214
|
+
.toBeVisible()
|
|
215
|
+
.withTimeout(5000);
|
|
216
|
+
|
|
217
|
+
// Handle animations
|
|
218
|
+
await element(by.id('button')).tap();
|
|
219
|
+
await waitFor(element(by.id('modal'))).toBeVisible();
|
|
220
|
+
|
|
221
|
+
// Scroll to elements
|
|
222
|
+
await element(by.id('scroll-view')).scrollTo('bottom');
|
|
223
|
+
await element(by.id('scroll-view')).scroll(200, 'down');
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Mocking
|
|
227
|
+
|
|
228
|
+
### Mock Native Modules
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// jest.setup.js
|
|
232
|
+
jest.mock('@react-native-async-storage/async-storage', () =>
|
|
233
|
+
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
jest.mock('react-native-device-info', () => ({
|
|
237
|
+
getVersion: () => '1.0.0',
|
|
238
|
+
getBuildNumber: () => '1',
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
jest.mock('@react-native-community/netinfo', () => ({
|
|
242
|
+
addEventListener: jest.fn(() => jest.fn()),
|
|
243
|
+
fetch: jest.fn(() => Promise.resolve({ isConnected: true })),
|
|
244
|
+
}));
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Mock Navigation
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
const mockNavigate = jest.fn();
|
|
251
|
+
|
|
252
|
+
jest.mock('@react-navigation/native', () => ({
|
|
253
|
+
...jest.requireActual('@react-navigation/native'),
|
|
254
|
+
useNavigation: () => ({
|
|
255
|
+
navigate: mockNavigate,
|
|
256
|
+
goBack: jest.fn(),
|
|
257
|
+
}),
|
|
258
|
+
}));
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Snapshot Testing
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// Use for complex UI that shouldn't change unintentionally
|
|
265
|
+
describe('ProductCard', () => {
|
|
266
|
+
it('matches snapshot', () => {
|
|
267
|
+
const tree = render(
|
|
268
|
+
<ProductCard
|
|
269
|
+
product={{
|
|
270
|
+
id: '1',
|
|
271
|
+
name: 'Test Product',
|
|
272
|
+
price: 29.99,
|
|
273
|
+
image: 'https://example.com/image.jpg',
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
).toJSON();
|
|
277
|
+
|
|
278
|
+
expect(tree).toMatchSnapshot();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Test Organization
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
src/
|
|
287
|
+
├── components/
|
|
288
|
+
│ ├── Button/
|
|
289
|
+
│ │ ├── Button.tsx
|
|
290
|
+
│ │ └── Button.test.tsx
|
|
291
|
+
│ └── ...
|
|
292
|
+
├── screens/
|
|
293
|
+
│ ├── Home/
|
|
294
|
+
│ │ ├── HomeScreen.tsx
|
|
295
|
+
│ │ └── HomeScreen.test.tsx
|
|
296
|
+
│ └── ...
|
|
297
|
+
├── hooks/
|
|
298
|
+
│ ├── useAuth.ts
|
|
299
|
+
│ └── useAuth.test.ts
|
|
300
|
+
└── utils/
|
|
301
|
+
├── formatters.ts
|
|
302
|
+
└── formatters.test.ts
|
|
303
|
+
|
|
304
|
+
e2e/
|
|
305
|
+
├── login.e2e.ts
|
|
306
|
+
├── checkout.e2e.ts
|
|
307
|
+
└── helpers/
|
|
308
|
+
└── testUtils.ts
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## CI/CD Testing
|
|
312
|
+
|
|
313
|
+
```yaml
|
|
314
|
+
# .github/workflows/test.yml
|
|
315
|
+
jobs:
|
|
316
|
+
unit-tests:
|
|
317
|
+
runs-on: ubuntu-latest
|
|
318
|
+
steps:
|
|
319
|
+
- uses: actions/checkout@v3
|
|
320
|
+
- uses: actions/setup-node@v3
|
|
321
|
+
- run: npm ci
|
|
322
|
+
- run: npm test -- --coverage
|
|
323
|
+
|
|
324
|
+
e2e-ios:
|
|
325
|
+
runs-on: macos-latest
|
|
326
|
+
steps:
|
|
327
|
+
- uses: actions/checkout@v3
|
|
328
|
+
- run: npm ci
|
|
329
|
+
- run: detox build --configuration ios.sim.release
|
|
330
|
+
- run: detox test --configuration ios.sim.release
|
|
331
|
+
|
|
332
|
+
e2e-android:
|
|
333
|
+
runs-on: macos-latest
|
|
334
|
+
steps:
|
|
335
|
+
- uses: actions/checkout@v3
|
|
336
|
+
- run: npm ci
|
|
337
|
+
- run: detox build --configuration android.emu.release
|
|
338
|
+
- run: detox test --configuration android.emu.release
|
|
339
|
+
```
|