@croacroa/react-native-template 2.0.1 → 3.2.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.
Files changed (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -0
  10. package/README.md +446 -399
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -0
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -175
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -1,362 +1,385 @@
1
- /**
2
- * @fileoverview Performance tests for VirtualizedList component
3
- * Tests render times and memory usage with large datasets.
4
- */
5
-
6
- import React from "react";
7
- import { render } from "@testing-library/react-native";
8
- import { View, Text } from "react-native";
9
-
10
- // Mock FlashList since it requires native modules
11
- jest.mock("@shopify/flash-list", () => {
12
- const { FlatList } = require("react-native");
13
- return {
14
- FlashList: FlatList,
15
- };
16
- });
17
-
18
- // Mock useTheme
19
- jest.mock("@/hooks/useTheme", () => ({
20
- useTheme: () => ({
21
- isDark: false,
22
- mode: "light",
23
- isLoaded: true,
24
- setMode: jest.fn(),
25
- toggleTheme: jest.fn(),
26
- }),
27
- }));
28
-
29
- import { VirtualizedList } from "@/components/ui/VirtualizedList";
30
-
31
- /**
32
- * Generate test data
33
- */
34
- function generateTestData(count: number): Array<{ id: string; title: string; value: number }> {
35
- return Array.from({ length: count }, (_, index) => ({
36
- id: `item-${index}`,
37
- title: `Item ${index}`,
38
- value: Math.random() * 1000,
39
- }));
40
- }
41
-
42
- /**
43
- * Simple render item component
44
- */
45
- function TestItem({ item }: { item: { id: string; title: string; value: number } }) {
46
- return (
47
- <View testID={`item-${item.id}`} style={{ height: 50, padding: 10 }}>
48
- <Text>{item.title}</Text>
49
- <Text>{item.value.toFixed(2)}</Text>
50
- </View>
51
- );
52
- }
53
-
54
- /**
55
- * Performance thresholds (in milliseconds)
56
- */
57
- const PERFORMANCE_THRESHOLDS = {
58
- SMALL_LIST_RENDER: 100, // 100 items
59
- MEDIUM_LIST_RENDER: 200, // 1000 items
60
- LARGE_LIST_RENDER: 500, // 10000 items
61
- INITIAL_RENDER: 50, // Initial render without data
62
- };
63
-
64
- describe("VirtualizedList Performance", () => {
65
- beforeEach(() => {
66
- jest.clearAllMocks();
67
- });
68
-
69
- describe("Render Time", () => {
70
- it("renders empty list within threshold", () => {
71
- const startTime = performance.now();
72
-
73
- render(
74
- <VirtualizedList
75
- data={[]}
76
- renderItem={({ item }) => <TestItem item={item} />}
77
- keyExtractor={(item) => item.id}
78
- estimatedItemSize={50}
79
- emptyMessage="No items"
80
- />
81
- );
82
-
83
- const renderTime = performance.now() - startTime;
84
-
85
- expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.INITIAL_RENDER);
86
- console.log(`Empty list render time: ${renderTime.toFixed(2)}ms`);
87
- });
88
-
89
- it("renders small list (100 items) within threshold", () => {
90
- const data = generateTestData(100);
91
- const startTime = performance.now();
92
-
93
- render(
94
- <VirtualizedList
95
- data={data}
96
- renderItem={({ item }) => <TestItem item={item} />}
97
- keyExtractor={(item) => item.id}
98
- estimatedItemSize={50}
99
- />
100
- );
101
-
102
- const renderTime = performance.now() - startTime;
103
-
104
- expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.SMALL_LIST_RENDER);
105
- console.log(`Small list (100 items) render time: ${renderTime.toFixed(2)}ms`);
106
- });
107
-
108
- it("renders medium list (1000 items) within threshold", () => {
109
- const data = generateTestData(1000);
110
- const startTime = performance.now();
111
-
112
- render(
113
- <VirtualizedList
114
- data={data}
115
- renderItem={({ item }) => <TestItem item={item} />}
116
- keyExtractor={(item) => item.id}
117
- estimatedItemSize={50}
118
- />
119
- );
120
-
121
- const renderTime = performance.now() - startTime;
122
-
123
- expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.MEDIUM_LIST_RENDER);
124
- console.log(`Medium list (1000 items) render time: ${renderTime.toFixed(2)}ms`);
125
- });
126
-
127
- it("renders large list (10000 items) within threshold", () => {
128
- const data = generateTestData(10000);
129
- const startTime = performance.now();
130
-
131
- render(
132
- <VirtualizedList
133
- data={data}
134
- renderItem={({ item }) => <TestItem item={item} />}
135
- keyExtractor={(item) => item.id}
136
- estimatedItemSize={50}
137
- />
138
- );
139
-
140
- const renderTime = performance.now() - startTime;
141
-
142
- expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.LARGE_LIST_RENDER);
143
- console.log(`Large list (10000 items) render time: ${renderTime.toFixed(2)}ms`);
144
- });
145
- });
146
-
147
- describe("Re-render Performance", () => {
148
- it("handles data updates efficiently", () => {
149
- const initialData = generateTestData(500);
150
-
151
- const { rerender } = render(
152
- <VirtualizedList
153
- data={initialData}
154
- renderItem={({ item }) => <TestItem item={item} />}
155
- keyExtractor={(item) => item.id}
156
- estimatedItemSize={50}
157
- />
158
- );
159
-
160
- // Measure re-render with new data
161
- const newData = generateTestData(500);
162
- const startTime = performance.now();
163
-
164
- rerender(
165
- <VirtualizedList
166
- data={newData}
167
- renderItem={({ item }) => <TestItem item={item} />}
168
- keyExtractor={(item) => item.id}
169
- estimatedItemSize={50}
170
- />
171
- );
172
-
173
- const rerenderTime = performance.now() - startTime;
174
-
175
- // Re-render should be fast since only visible items are rendered
176
- expect(rerenderTime).toBeLessThan(100);
177
- console.log(`Re-render time (500 items): ${rerenderTime.toFixed(2)}ms`);
178
- });
179
-
180
- it("handles appending items efficiently", () => {
181
- const initialData = generateTestData(100);
182
-
183
- const { rerender } = render(
184
- <VirtualizedList
185
- data={initialData}
186
- renderItem={({ item }) => <TestItem item={item} />}
187
- keyExtractor={(item) => item.id}
188
- estimatedItemSize={50}
189
- />
190
- );
191
-
192
- // Append 100 more items
193
- const appendedData = [...initialData, ...generateTestData(100).map((item, i) => ({
194
- ...item,
195
- id: `appended-${i}`,
196
- }))];
197
-
198
- const startTime = performance.now();
199
-
200
- rerender(
201
- <VirtualizedList
202
- data={appendedData}
203
- renderItem={({ item }) => <TestItem item={item} />}
204
- keyExtractor={(item) => item.id}
205
- estimatedItemSize={50}
206
- />
207
- );
208
-
209
- const appendTime = performance.now() - startTime;
210
-
211
- expect(appendTime).toBeLessThan(50);
212
- console.log(`Append time (100 items): ${appendTime.toFixed(2)}ms`);
213
- });
214
- });
215
-
216
- describe("Memory Efficiency", () => {
217
- it("maintains stable render count with static data", () => {
218
- let renderCount = 0;
219
-
220
- const CountingItem = ({ item }: { item: { id: string; title: string; value: number } }) => {
221
- renderCount++;
222
- return <TestItem item={item} />;
223
- };
224
-
225
- const data = generateTestData(100);
226
-
227
- const { rerender } = render(
228
- <VirtualizedList
229
- data={data}
230
- renderItem={({ item }) => <CountingItem item={item} />}
231
- keyExtractor={(item) => item.id}
232
- estimatedItemSize={50}
233
- />
234
- );
235
-
236
- const initialRenderCount = renderCount;
237
-
238
- // Re-render with same data
239
- rerender(
240
- <VirtualizedList
241
- data={data}
242
- renderItem={({ item }) => <CountingItem item={item} />}
243
- keyExtractor={(item) => item.id}
244
- estimatedItemSize={50}
245
- />
246
- );
247
-
248
- // With proper memoization, render count should not double
249
- // Allow some flexibility for visible items being re-rendered
250
- expect(renderCount).toBeLessThan(initialRenderCount * 2);
251
- console.log(`Initial renders: ${initialRenderCount}, After re-render: ${renderCount}`);
252
- });
253
- });
254
-
255
- describe("Props Validation", () => {
256
- it("handles all size datasets without crashing", () => {
257
- const sizes = [0, 1, 10, 100, 1000, 5000];
258
-
259
- for (const size of sizes) {
260
- const data = generateTestData(size);
261
-
262
- expect(() => {
263
- render(
264
- <VirtualizedList
265
- data={data}
266
- renderItem={({ item }) => <TestItem item={item} />}
267
- keyExtractor={(item) => item.id}
268
- estimatedItemSize={50}
269
- />
270
- );
271
- }).not.toThrow();
272
- }
273
- });
274
-
275
- it("handles undefined/null items gracefully", () => {
276
- const data = [
277
- { id: "1", title: "Item 1", value: 100 },
278
- { id: "2", title: "Item 2", value: 200 },
279
- ];
280
-
281
- expect(() => {
282
- render(
283
- <VirtualizedList
284
- data={data}
285
- renderItem={({ item }) => (item ? <TestItem item={item} /> : null)}
286
- keyExtractor={(item) => item?.id || "unknown"}
287
- estimatedItemSize={50}
288
- />
289
- );
290
- }).not.toThrow();
291
- });
292
- });
293
- });
294
-
295
- describe("Performance Metrics Summary", () => {
296
- it("logs performance summary", () => {
297
- const metrics = {
298
- emptyList: 0,
299
- smallList: 0,
300
- mediumList: 0,
301
- largeList: 0,
302
- };
303
-
304
- // Empty list
305
- let start = performance.now();
306
- render(
307
- <VirtualizedList
308
- data={[]}
309
- renderItem={({ item }) => <TestItem item={item} />}
310
- keyExtractor={(item) => item.id}
311
- estimatedItemSize={50}
312
- />
313
- );
314
- metrics.emptyList = performance.now() - start;
315
-
316
- // Small list
317
- start = performance.now();
318
- render(
319
- <VirtualizedList
320
- data={generateTestData(100)}
321
- renderItem={({ item }) => <TestItem item={item} />}
322
- keyExtractor={(item) => item.id}
323
- estimatedItemSize={50}
324
- />
325
- );
326
- metrics.smallList = performance.now() - start;
327
-
328
- // Medium list
329
- start = performance.now();
330
- render(
331
- <VirtualizedList
332
- data={generateTestData(1000)}
333
- renderItem={({ item }) => <TestItem item={item} />}
334
- keyExtractor={(item) => item.id}
335
- estimatedItemSize={50}
336
- />
337
- );
338
- metrics.mediumList = performance.now() - start;
339
-
340
- // Large list
341
- start = performance.now();
342
- render(
343
- <VirtualizedList
344
- data={generateTestData(10000)}
345
- renderItem={({ item }) => <TestItem item={item} />}
346
- keyExtractor={(item) => item.id}
347
- estimatedItemSize={50}
348
- />
349
- );
350
- metrics.largeList = performance.now() - start;
351
-
352
- console.log("\n=== VirtualizedList Performance Summary ===");
353
- console.log(`Empty list: ${metrics.emptyList.toFixed(2)}ms`);
354
- console.log(`Small (100): ${metrics.smallList.toFixed(2)}ms`);
355
- console.log(`Medium (1000): ${metrics.mediumList.toFixed(2)}ms`);
356
- console.log(`Large (10000): ${metrics.largeList.toFixed(2)}ms`);
357
- console.log("==========================================\n");
358
-
359
- // This test always passes, it's just for logging
360
- expect(true).toBe(true);
361
- });
362
- });
1
+ /**
2
+ * @fileoverview Performance tests for VirtualizedList component
3
+ * Tests render times and memory usage with large datasets.
4
+ */
5
+
6
+ import React from "react";
7
+ import { render } from "@testing-library/react-native";
8
+ import { View, Text } from "react-native";
9
+
10
+ import { VirtualizedList } from "@/components/ui/VirtualizedList";
11
+
12
+ // Mock FlashList since it requires native modules
13
+ jest.mock("@shopify/flash-list", () => {
14
+ const { FlatList } = require("react-native");
15
+ return {
16
+ FlashList: FlatList,
17
+ };
18
+ });
19
+
20
+ // Mock useTheme
21
+ jest.mock("@/hooks/useTheme", () => ({
22
+ useTheme: () => ({
23
+ isDark: false,
24
+ mode: "light",
25
+ isLoaded: true,
26
+ setMode: jest.fn(),
27
+ toggleTheme: jest.fn(),
28
+ }),
29
+ }));
30
+
31
+ /**
32
+ * Generate test data
33
+ */
34
+ function generateTestData(
35
+ count: number
36
+ ): { id: string; title: string; value: number }[] {
37
+ return Array.from({ length: count }, (_, index) => ({
38
+ id: `item-${index}`,
39
+ title: `Item ${index}`,
40
+ value: Math.random() * 1000,
41
+ }));
42
+ }
43
+
44
+ /**
45
+ * Simple render item component
46
+ */
47
+ function TestItem({
48
+ item,
49
+ }: {
50
+ item: { id: string; title: string; value: number };
51
+ }) {
52
+ return (
53
+ <View testID={`item-${item.id}`} style={{ height: 50, padding: 10 }}>
54
+ <Text>{item.title}</Text>
55
+ <Text>{item.value.toFixed(2)}</Text>
56
+ </View>
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Performance thresholds (in milliseconds)
62
+ */
63
+ const PERFORMANCE_THRESHOLDS = {
64
+ SMALL_LIST_RENDER: 100, // 100 items
65
+ MEDIUM_LIST_RENDER: 200, // 1000 items
66
+ LARGE_LIST_RENDER: 500, // 10000 items
67
+ INITIAL_RENDER: 50, // Initial render without data
68
+ };
69
+
70
+ describe("VirtualizedList Performance", () => {
71
+ beforeEach(() => {
72
+ jest.clearAllMocks();
73
+ });
74
+
75
+ describe("Render Time", () => {
76
+ it("renders empty list within threshold", () => {
77
+ const startTime = performance.now();
78
+
79
+ render(
80
+ <VirtualizedList
81
+ data={[]}
82
+ renderItem={({ item }) => <TestItem item={item} />}
83
+ keyExtractor={(item) => item.id}
84
+ estimatedItemSize={50}
85
+ emptyMessage="No items"
86
+ />
87
+ );
88
+
89
+ const renderTime = performance.now() - startTime;
90
+
91
+ expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.INITIAL_RENDER);
92
+ console.log(`Empty list render time: ${renderTime.toFixed(2)}ms`);
93
+ });
94
+
95
+ it("renders small list (100 items) within threshold", () => {
96
+ const data = generateTestData(100);
97
+ const startTime = performance.now();
98
+
99
+ render(
100
+ <VirtualizedList
101
+ data={data}
102
+ renderItem={({ item }) => <TestItem item={item} />}
103
+ keyExtractor={(item) => item.id}
104
+ estimatedItemSize={50}
105
+ />
106
+ );
107
+
108
+ const renderTime = performance.now() - startTime;
109
+
110
+ expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.SMALL_LIST_RENDER);
111
+ console.log(
112
+ `Small list (100 items) render time: ${renderTime.toFixed(2)}ms`
113
+ );
114
+ });
115
+
116
+ it("renders medium list (1000 items) within threshold", () => {
117
+ const data = generateTestData(1000);
118
+ const startTime = performance.now();
119
+
120
+ render(
121
+ <VirtualizedList
122
+ data={data}
123
+ renderItem={({ item }) => <TestItem item={item} />}
124
+ keyExtractor={(item) => item.id}
125
+ estimatedItemSize={50}
126
+ />
127
+ );
128
+
129
+ const renderTime = performance.now() - startTime;
130
+
131
+ expect(renderTime).toBeLessThan(
132
+ PERFORMANCE_THRESHOLDS.MEDIUM_LIST_RENDER
133
+ );
134
+ console.log(
135
+ `Medium list (1000 items) render time: ${renderTime.toFixed(2)}ms`
136
+ );
137
+ });
138
+
139
+ it("renders large list (10000 items) within threshold", () => {
140
+ const data = generateTestData(10000);
141
+ const startTime = performance.now();
142
+
143
+ render(
144
+ <VirtualizedList
145
+ data={data}
146
+ renderItem={({ item }) => <TestItem item={item} />}
147
+ keyExtractor={(item) => item.id}
148
+ estimatedItemSize={50}
149
+ />
150
+ );
151
+
152
+ const renderTime = performance.now() - startTime;
153
+
154
+ expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.LARGE_LIST_RENDER);
155
+ console.log(
156
+ `Large list (10000 items) render time: ${renderTime.toFixed(2)}ms`
157
+ );
158
+ });
159
+ });
160
+
161
+ describe("Re-render Performance", () => {
162
+ it("handles data updates efficiently", () => {
163
+ const initialData = generateTestData(500);
164
+
165
+ const { rerender } = render(
166
+ <VirtualizedList
167
+ data={initialData}
168
+ renderItem={({ item }) => <TestItem item={item} />}
169
+ keyExtractor={(item) => item.id}
170
+ estimatedItemSize={50}
171
+ />
172
+ );
173
+
174
+ // Measure re-render with new data
175
+ const newData = generateTestData(500);
176
+ const startTime = performance.now();
177
+
178
+ rerender(
179
+ <VirtualizedList
180
+ data={newData}
181
+ renderItem={({ item }) => <TestItem item={item} />}
182
+ keyExtractor={(item) => item.id}
183
+ estimatedItemSize={50}
184
+ />
185
+ );
186
+
187
+ const rerenderTime = performance.now() - startTime;
188
+
189
+ // Re-render should be fast since only visible items are rendered
190
+ expect(rerenderTime).toBeLessThan(100);
191
+ console.log(`Re-render time (500 items): ${rerenderTime.toFixed(2)}ms`);
192
+ });
193
+
194
+ it("handles appending items efficiently", () => {
195
+ const initialData = generateTestData(100);
196
+
197
+ const { rerender } = render(
198
+ <VirtualizedList
199
+ data={initialData}
200
+ renderItem={({ item }) => <TestItem item={item} />}
201
+ keyExtractor={(item) => item.id}
202
+ estimatedItemSize={50}
203
+ />
204
+ );
205
+
206
+ // Append 100 more items
207
+ const appendedData = [
208
+ ...initialData,
209
+ ...generateTestData(100).map((item, i) => ({
210
+ ...item,
211
+ id: `appended-${i}`,
212
+ })),
213
+ ];
214
+
215
+ const startTime = performance.now();
216
+
217
+ rerender(
218
+ <VirtualizedList
219
+ data={appendedData}
220
+ renderItem={({ item }) => <TestItem item={item} />}
221
+ keyExtractor={(item) => item.id}
222
+ estimatedItemSize={50}
223
+ />
224
+ );
225
+
226
+ const appendTime = performance.now() - startTime;
227
+
228
+ expect(appendTime).toBeLessThan(50);
229
+ console.log(`Append time (100 items): ${appendTime.toFixed(2)}ms`);
230
+ });
231
+ });
232
+
233
+ describe("Memory Efficiency", () => {
234
+ it("maintains stable render count with static data", () => {
235
+ let renderCount = 0;
236
+
237
+ const CountingItem = ({
238
+ item,
239
+ }: {
240
+ item: { id: string; title: string; value: number };
241
+ }) => {
242
+ renderCount++;
243
+ return <TestItem item={item} />;
244
+ };
245
+
246
+ const data = generateTestData(100);
247
+
248
+ const { rerender } = render(
249
+ <VirtualizedList
250
+ data={data}
251
+ renderItem={({ item }) => <CountingItem item={item} />}
252
+ keyExtractor={(item) => item.id}
253
+ estimatedItemSize={50}
254
+ />
255
+ );
256
+
257
+ const initialRenderCount = renderCount;
258
+
259
+ // Re-render with same data
260
+ rerender(
261
+ <VirtualizedList
262
+ data={data}
263
+ renderItem={({ item }) => <CountingItem item={item} />}
264
+ keyExtractor={(item) => item.id}
265
+ estimatedItemSize={50}
266
+ />
267
+ );
268
+
269
+ // With proper memoization, render count should not double
270
+ // Allow some flexibility for visible items being re-rendered
271
+ expect(renderCount).toBeLessThan(initialRenderCount * 2);
272
+ console.log(
273
+ `Initial renders: ${initialRenderCount}, After re-render: ${renderCount}`
274
+ );
275
+ });
276
+ });
277
+
278
+ describe("Props Validation", () => {
279
+ it("handles all size datasets without crashing", () => {
280
+ const sizes = [0, 1, 10, 100, 1000, 5000];
281
+
282
+ for (const size of sizes) {
283
+ const data = generateTestData(size);
284
+
285
+ expect(() => {
286
+ render(
287
+ <VirtualizedList
288
+ data={data}
289
+ renderItem={({ item }) => <TestItem item={item} />}
290
+ keyExtractor={(item) => item.id}
291
+ estimatedItemSize={50}
292
+ />
293
+ );
294
+ }).not.toThrow();
295
+ }
296
+ });
297
+
298
+ it("handles undefined/null items gracefully", () => {
299
+ const data = [
300
+ { id: "1", title: "Item 1", value: 100 },
301
+ { id: "2", title: "Item 2", value: 200 },
302
+ ];
303
+
304
+ expect(() => {
305
+ render(
306
+ <VirtualizedList
307
+ data={data}
308
+ renderItem={({ item }) => (item ? <TestItem item={item} /> : null)}
309
+ keyExtractor={(item) => item?.id || "unknown"}
310
+ estimatedItemSize={50}
311
+ />
312
+ );
313
+ }).not.toThrow();
314
+ });
315
+ });
316
+ });
317
+
318
+ describe("Performance Metrics Summary", () => {
319
+ it("logs performance summary", () => {
320
+ const metrics = {
321
+ emptyList: 0,
322
+ smallList: 0,
323
+ mediumList: 0,
324
+ largeList: 0,
325
+ };
326
+
327
+ // Empty list
328
+ let start = performance.now();
329
+ render(
330
+ <VirtualizedList
331
+ data={[]}
332
+ renderItem={({ item }) => <TestItem item={item} />}
333
+ keyExtractor={(item) => item.id}
334
+ estimatedItemSize={50}
335
+ />
336
+ );
337
+ metrics.emptyList = performance.now() - start;
338
+
339
+ // Small list
340
+ start = performance.now();
341
+ render(
342
+ <VirtualizedList
343
+ data={generateTestData(100)}
344
+ renderItem={({ item }) => <TestItem item={item} />}
345
+ keyExtractor={(item) => item.id}
346
+ estimatedItemSize={50}
347
+ />
348
+ );
349
+ metrics.smallList = performance.now() - start;
350
+
351
+ // Medium list
352
+ start = performance.now();
353
+ render(
354
+ <VirtualizedList
355
+ data={generateTestData(1000)}
356
+ renderItem={({ item }) => <TestItem item={item} />}
357
+ keyExtractor={(item) => item.id}
358
+ estimatedItemSize={50}
359
+ />
360
+ );
361
+ metrics.mediumList = performance.now() - start;
362
+
363
+ // Large list
364
+ start = performance.now();
365
+ render(
366
+ <VirtualizedList
367
+ data={generateTestData(10000)}
368
+ renderItem={({ item }) => <TestItem item={item} />}
369
+ keyExtractor={(item) => item.id}
370
+ estimatedItemSize={50}
371
+ />
372
+ );
373
+ metrics.largeList = performance.now() - start;
374
+
375
+ console.log("\n=== VirtualizedList Performance Summary ===");
376
+ console.log(`Empty list: ${metrics.emptyList.toFixed(2)}ms`);
377
+ console.log(`Small (100): ${metrics.smallList.toFixed(2)}ms`);
378
+ console.log(`Medium (1000): ${metrics.mediumList.toFixed(2)}ms`);
379
+ console.log(`Large (10000): ${metrics.largeList.toFixed(2)}ms`);
380
+ console.log("==========================================\n");
381
+
382
+ // This test always passes, it's just for logging
383
+ expect(true).toBe(true);
384
+ });
385
+ });