@croacroa/react-native-template 1.0.0 → 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.
Files changed (69) hide show
  1. package/.github/workflows/ci.yml +187 -184
  2. package/.github/workflows/eas-build.yml +55 -55
  3. package/.github/workflows/eas-update.yml +50 -50
  4. package/CHANGELOG.md +106 -106
  5. package/CONTRIBUTING.md +377 -377
  6. package/README.md +399 -399
  7. package/__tests__/components/snapshots.test.tsx +131 -0
  8. package/__tests__/integration/auth-api.test.tsx +227 -0
  9. package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
  10. package/app/(public)/onboarding.tsx +5 -5
  11. package/app.config.ts +45 -2
  12. package/assets/images/.gitkeep +7 -7
  13. package/components/onboarding/OnboardingScreen.tsx +370 -370
  14. package/components/onboarding/index.ts +2 -2
  15. package/components/providers/SuspenseBoundary.tsx +357 -0
  16. package/components/providers/index.ts +13 -0
  17. package/components/ui/Avatar.tsx +316 -316
  18. package/components/ui/Badge.tsx +416 -416
  19. package/components/ui/BottomSheet.tsx +307 -307
  20. package/components/ui/Checkbox.tsx +261 -261
  21. package/components/ui/OptimizedImage.tsx +369 -369
  22. package/components/ui/Select.tsx +240 -240
  23. package/components/ui/VirtualizedList.tsx +285 -0
  24. package/components/ui/index.ts +23 -18
  25. package/constants/config.ts +97 -54
  26. package/docs/adr/001-state-management.md +79 -79
  27. package/docs/adr/002-styling-approach.md +130 -130
  28. package/docs/adr/003-data-fetching.md +155 -155
  29. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  30. package/docs/adr/README.md +78 -78
  31. package/hooks/index.ts +27 -25
  32. package/hooks/useApi.ts +102 -5
  33. package/hooks/useAuth.tsx +82 -0
  34. package/hooks/useBiometrics.ts +295 -295
  35. package/hooks/useDeepLinking.ts +256 -256
  36. package/hooks/useMFA.ts +499 -0
  37. package/hooks/useNotifications.ts +39 -0
  38. package/hooks/useOffline.ts +32 -2
  39. package/hooks/usePerformance.ts +434 -434
  40. package/hooks/useTheme.tsx +76 -0
  41. package/hooks/useUpdates.ts +358 -358
  42. package/i18n/index.ts +194 -77
  43. package/i18n/locales/ar.json +101 -0
  44. package/i18n/locales/de.json +101 -0
  45. package/i18n/locales/en.json +101 -101
  46. package/i18n/locales/es.json +101 -0
  47. package/i18n/locales/fr.json +101 -101
  48. package/jest.config.js +4 -4
  49. package/maestro/README.md +113 -113
  50. package/maestro/config.yaml +35 -35
  51. package/maestro/flows/login.yaml +62 -62
  52. package/maestro/flows/mfa-login.yaml +92 -0
  53. package/maestro/flows/mfa-setup.yaml +86 -0
  54. package/maestro/flows/navigation.yaml +68 -68
  55. package/maestro/flows/offline-conflict.yaml +101 -0
  56. package/maestro/flows/offline-sync.yaml +128 -0
  57. package/maestro/flows/offline.yaml +60 -60
  58. package/maestro/flows/register.yaml +94 -94
  59. package/package.json +175 -170
  60. package/services/analytics.ts +428 -428
  61. package/services/api.ts +340 -340
  62. package/services/authAdapter.ts +333 -333
  63. package/services/backgroundSync.ts +626 -0
  64. package/services/index.ts +54 -22
  65. package/services/security.ts +229 -0
  66. package/tailwind.config.js +47 -47
  67. package/utils/accessibility.ts +446 -446
  68. package/utils/index.ts +52 -43
  69. package/utils/withAccessibility.tsx +272 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @fileoverview Snapshot tests for UI components
3
+ * Ensures visual consistency of components across changes.
4
+ */
5
+
6
+ import React from "react";
7
+ import { render } from "@testing-library/react-native";
8
+ import { Text, View } from "react-native";
9
+
10
+ import { Button } from "@/components/ui/Button";
11
+ import { Card } from "@/components/ui/Card";
12
+
13
+ // Mock useTheme hook for components that use it
14
+ jest.mock("@/hooks/useTheme", () => ({
15
+ useTheme: () => ({
16
+ isDark: false,
17
+ mode: "light",
18
+ isLoaded: true,
19
+ setMode: jest.fn(),
20
+ toggleTheme: jest.fn(),
21
+ }),
22
+ }));
23
+
24
+ describe("UI Components Snapshots", () => {
25
+ describe("Button", () => {
26
+ it("renders primary variant correctly", () => {
27
+ const { toJSON } = render(<Button variant="primary">Primary</Button>);
28
+ expect(toJSON()).toMatchSnapshot();
29
+ });
30
+
31
+ it("renders secondary variant correctly", () => {
32
+ const { toJSON } = render(<Button variant="secondary">Secondary</Button>);
33
+ expect(toJSON()).toMatchSnapshot();
34
+ });
35
+
36
+ it("renders outline variant correctly", () => {
37
+ const { toJSON } = render(<Button variant="outline">Outline</Button>);
38
+ expect(toJSON()).toMatchSnapshot();
39
+ });
40
+
41
+ it("renders ghost variant correctly", () => {
42
+ const { toJSON } = render(<Button variant="ghost">Ghost</Button>);
43
+ expect(toJSON()).toMatchSnapshot();
44
+ });
45
+
46
+ it("renders danger variant correctly", () => {
47
+ const { toJSON } = render(<Button variant="danger">Danger</Button>);
48
+ expect(toJSON()).toMatchSnapshot();
49
+ });
50
+
51
+ it("renders small size correctly", () => {
52
+ const { toJSON } = render(<Button size="sm">Small</Button>);
53
+ expect(toJSON()).toMatchSnapshot();
54
+ });
55
+
56
+ it("renders large size correctly", () => {
57
+ const { toJSON } = render(<Button size="lg">Large</Button>);
58
+ expect(toJSON()).toMatchSnapshot();
59
+ });
60
+
61
+ it("renders loading state correctly", () => {
62
+ const { toJSON } = render(<Button isLoading>Loading</Button>);
63
+ expect(toJSON()).toMatchSnapshot();
64
+ });
65
+
66
+ it("renders disabled state correctly", () => {
67
+ const { toJSON } = render(<Button disabled>Disabled</Button>);
68
+ expect(toJSON()).toMatchSnapshot();
69
+ });
70
+
71
+ it("renders with custom children correctly", () => {
72
+ const { toJSON } = render(
73
+ <Button>
74
+ <View>
75
+ <Text>Custom Content</Text>
76
+ </View>
77
+ </Button>
78
+ );
79
+ expect(toJSON()).toMatchSnapshot();
80
+ });
81
+ });
82
+
83
+ describe("Card", () => {
84
+ it("renders default variant correctly", () => {
85
+ const { toJSON } = render(
86
+ <Card>
87
+ <Text>Default Card</Text>
88
+ </Card>
89
+ );
90
+ expect(toJSON()).toMatchSnapshot();
91
+ });
92
+
93
+ it("renders elevated variant correctly", () => {
94
+ const { toJSON } = render(
95
+ <Card variant="elevated">
96
+ <Text>Elevated Card</Text>
97
+ </Card>
98
+ );
99
+ expect(toJSON()).toMatchSnapshot();
100
+ });
101
+
102
+ it("renders outlined variant correctly", () => {
103
+ const { toJSON } = render(
104
+ <Card variant="outlined">
105
+ <Text>Outlined Card</Text>
106
+ </Card>
107
+ );
108
+ expect(toJSON()).toMatchSnapshot();
109
+ });
110
+
111
+ it("renders with custom className correctly", () => {
112
+ const { toJSON } = render(
113
+ <Card className="p-4 m-2">
114
+ <Text>Custom Styled Card</Text>
115
+ </Card>
116
+ );
117
+ expect(toJSON()).toMatchSnapshot();
118
+ });
119
+
120
+ it("renders complex content correctly", () => {
121
+ const { toJSON } = render(
122
+ <Card variant="elevated" className="p-4">
123
+ <Text>Title</Text>
124
+ <Text>Description text here</Text>
125
+ <Button>Action</Button>
126
+ </Card>
127
+ );
128
+ expect(toJSON()).toMatchSnapshot();
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @fileoverview Integration tests for authentication and API flow
3
+ * Tests the complete flow from sign-in through authenticated API calls.
4
+ */
5
+
6
+ import React from "react";
7
+ import { render, waitFor, act } from "@testing-library/react-native";
8
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9
+ import { AuthProvider, useAuth, getAuthToken } from "@/hooks/useAuth";
10
+ import { api } from "@/services/api";
11
+
12
+ // Mock SecureStore
13
+ const mockSecureStore: Record<string, string> = {};
14
+ jest.mock("expo-secure-store", () => ({
15
+ getItemAsync: jest.fn((key: string) =>
16
+ Promise.resolve(mockSecureStore[key] || null)
17
+ ),
18
+ setItemAsync: jest.fn((key: string, value: string) => {
19
+ mockSecureStore[key] = value;
20
+ return Promise.resolve();
21
+ }),
22
+ deleteItemAsync: jest.fn((key: string) => {
23
+ delete mockSecureStore[key];
24
+ return Promise.resolve();
25
+ }),
26
+ }));
27
+
28
+ // Mock expo-router
29
+ jest.mock("expo-router", () => ({
30
+ router: {
31
+ replace: jest.fn(),
32
+ push: jest.fn(),
33
+ },
34
+ }));
35
+
36
+ // Mock toast
37
+ jest.mock("@/utils/toast", () => ({
38
+ toast: {
39
+ success: jest.fn(),
40
+ error: jest.fn(),
41
+ info: jest.fn(),
42
+ },
43
+ handleApiError: jest.fn(),
44
+ }));
45
+
46
+ // Test component that uses auth
47
+ function TestAuthComponent({
48
+ onAuthState,
49
+ }: {
50
+ onAuthState: (state: {
51
+ user: unknown;
52
+ isAuthenticated: boolean;
53
+ isLoading: boolean;
54
+ }) => void;
55
+ }) {
56
+ const auth = useAuth();
57
+
58
+ React.useEffect(() => {
59
+ onAuthState({
60
+ user: auth.user,
61
+ isAuthenticated: auth.isAuthenticated,
62
+ isLoading: auth.isLoading,
63
+ });
64
+ }, [auth.user, auth.isAuthenticated, auth.isLoading, onAuthState]);
65
+
66
+ return null;
67
+ }
68
+
69
+ // Wrapper with providers
70
+ function createWrapper() {
71
+ const queryClient = new QueryClient({
72
+ defaultOptions: {
73
+ queries: {
74
+ retry: false,
75
+ },
76
+ },
77
+ });
78
+
79
+ return function Wrapper({ children }: { children: React.ReactNode }) {
80
+ return (
81
+ <QueryClientProvider client={queryClient}>
82
+ <AuthProvider>{children}</AuthProvider>
83
+ </QueryClientProvider>
84
+ );
85
+ };
86
+ }
87
+
88
+ describe("Auth + API Integration", () => {
89
+ beforeEach(() => {
90
+ // Clear mock storage
91
+ Object.keys(mockSecureStore).forEach((key) => delete mockSecureStore[key]);
92
+ jest.clearAllMocks();
93
+ });
94
+
95
+ describe("Authentication Flow", () => {
96
+ it("starts with loading state then transitions to unauthenticated", async () => {
97
+ const states: Array<{ isLoading: boolean; isAuthenticated: boolean }> =
98
+ [];
99
+
100
+ render(
101
+ <TestAuthComponent
102
+ onAuthState={(state) => states.push(state)}
103
+ />,
104
+ { wrapper: createWrapper() }
105
+ );
106
+
107
+ await waitFor(() => {
108
+ expect(states.length).toBeGreaterThanOrEqual(2);
109
+ });
110
+
111
+ // First state should be loading
112
+ expect(states[0].isLoading).toBe(true);
113
+
114
+ // Final state should be not loading and not authenticated
115
+ const finalState = states[states.length - 1];
116
+ expect(finalState.isLoading).toBe(false);
117
+ expect(finalState.isAuthenticated).toBe(false);
118
+ });
119
+
120
+ it("restores session from stored tokens", async () => {
121
+ // Pre-populate storage with valid tokens
122
+ const tokens = {
123
+ accessToken: "stored_access_token",
124
+ refreshToken: "stored_refresh_token",
125
+ expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
126
+ };
127
+ const user = {
128
+ id: "1",
129
+ email: "stored@example.com",
130
+ name: "Stored User",
131
+ };
132
+
133
+ mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
134
+ mockSecureStore["auth_user"] = JSON.stringify(user);
135
+
136
+ let finalState: { user: unknown; isAuthenticated: boolean } | null = null;
137
+
138
+ render(
139
+ <TestAuthComponent
140
+ onAuthState={(state) => {
141
+ if (!state.isLoading) {
142
+ finalState = state;
143
+ }
144
+ }}
145
+ />,
146
+ { wrapper: createWrapper() }
147
+ );
148
+
149
+ await waitFor(() => {
150
+ expect(finalState).not.toBeNull();
151
+ });
152
+
153
+ expect(finalState?.isAuthenticated).toBe(true);
154
+ expect(finalState?.user).toEqual(user);
155
+ });
156
+
157
+ it("handles expired tokens by refreshing", async () => {
158
+ // Pre-populate storage with expired tokens
159
+ const tokens = {
160
+ accessToken: "expired_access_token",
161
+ refreshToken: "valid_refresh_token",
162
+ expiresAt: Date.now() - 1000, // Expired 1 second ago
163
+ };
164
+ const user = {
165
+ id: "1",
166
+ email: "test@example.com",
167
+ name: "Test User",
168
+ };
169
+
170
+ mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
171
+ mockSecureStore["auth_user"] = JSON.stringify(user);
172
+
173
+ let finalState: { isAuthenticated: boolean } | null = null;
174
+
175
+ render(
176
+ <TestAuthComponent
177
+ onAuthState={(state) => {
178
+ if (!state.isLoading) {
179
+ finalState = state;
180
+ }
181
+ }}
182
+ />,
183
+ { wrapper: createWrapper() }
184
+ );
185
+
186
+ await waitFor(
187
+ () => {
188
+ expect(finalState).not.toBeNull();
189
+ },
190
+ { timeout: 3000 }
191
+ );
192
+
193
+ // Should be authenticated after token refresh (mock implementation always succeeds)
194
+ expect(finalState?.isAuthenticated).toBe(true);
195
+ });
196
+ });
197
+
198
+ describe("Token Management", () => {
199
+ it("getAuthToken returns null when no tokens stored", async () => {
200
+ const token = await getAuthToken();
201
+ expect(token).toBeNull();
202
+ });
203
+
204
+ it("getAuthToken returns access token when stored", async () => {
205
+ const tokens = {
206
+ accessToken: "test_access_token",
207
+ refreshToken: "test_refresh_token",
208
+ expiresAt: Date.now() + 60 * 60 * 1000,
209
+ };
210
+
211
+ mockSecureStore["auth_tokens"] = JSON.stringify(tokens);
212
+
213
+ const token = await getAuthToken();
214
+ expect(token).toBe("test_access_token");
215
+ });
216
+ });
217
+
218
+ describe("API Client Integration", () => {
219
+ it("api client exists and has required methods", () => {
220
+ expect(api).toBeDefined();
221
+ expect(typeof api.get).toBe("function");
222
+ expect(typeof api.post).toBe("function");
223
+ expect(typeof api.patch).toBe("function");
224
+ expect(typeof api.delete).toBe("function");
225
+ });
226
+ });
227
+ });
@@ -0,0 +1,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
+ // 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
+ });