@umituz/react-native-design-system 2.3.14 → 2.3.16

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 (93) hide show
  1. package/package.json +19 -2
  2. package/src/index.ts +105 -0
  3. package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +2 -2
  4. package/src/layouts/ScreenLayout/ScreenLayout.tsx +1 -1
  5. package/src/molecules/animation/core/AnimationCore.ts +29 -0
  6. package/src/molecules/animation/domain/entities/Animation.ts +81 -0
  7. package/src/molecules/animation/domain/entities/Fireworks.ts +44 -0
  8. package/src/molecules/animation/domain/entities/Theme.ts +76 -0
  9. package/src/molecules/animation/index.ts +146 -0
  10. package/src/molecules/animation/infrastructure/services/AnimationConfigService.ts +35 -0
  11. package/src/molecules/animation/infrastructure/services/SpringAnimationConfigService.ts +67 -0
  12. package/src/molecules/animation/infrastructure/services/TimingAnimationConfigService.ts +57 -0
  13. package/src/molecules/animation/infrastructure/services/__tests__/SpringAnimationConfigService.test.ts +114 -0
  14. package/src/molecules/animation/infrastructure/services/__tests__/TimingAnimationConfigService.test.ts +105 -0
  15. package/src/molecules/animation/presentation/components/Fireworks.tsx +126 -0
  16. package/src/molecules/animation/presentation/components/__tests__/Fireworks.test.tsx +189 -0
  17. package/src/molecules/animation/presentation/hooks/__tests__/useAnimation.integration.test.ts +216 -0
  18. package/src/molecules/animation/presentation/hooks/__tests__/useFireworks.test.ts +242 -0
  19. package/src/molecules/animation/presentation/hooks/__tests__/useGesture.test.ts +111 -0
  20. package/src/molecules/animation/presentation/hooks/__tests__/useSpringAnimation.test.ts +131 -0
  21. package/src/molecules/animation/presentation/hooks/__tests__/useTimingAnimation.test.ts +175 -0
  22. package/src/molecules/animation/presentation/hooks/__tests__/useTransformAnimation.test.ts +137 -0
  23. package/src/molecules/animation/presentation/hooks/useAnimation.ts +77 -0
  24. package/src/molecules/animation/presentation/hooks/useFireworks.ts +141 -0
  25. package/src/molecules/animation/presentation/hooks/useGesture.ts +61 -0
  26. package/src/molecules/animation/presentation/hooks/useGestureCreators.ts +163 -0
  27. package/src/molecules/animation/presentation/hooks/useGestureState.ts +53 -0
  28. package/src/molecules/animation/presentation/hooks/useIconAnimations.ts +119 -0
  29. package/src/molecules/animation/presentation/hooks/useModalAnimations.ts +124 -0
  30. package/src/molecules/animation/presentation/hooks/useReanimatedReady.ts +60 -0
  31. package/src/molecules/animation/presentation/hooks/useSpringAnimation.ts +69 -0
  32. package/src/molecules/animation/presentation/hooks/useTimingAnimation.ts +111 -0
  33. package/src/molecules/animation/presentation/hooks/useTransformAnimation.ts +57 -0
  34. package/src/molecules/animation/presentation/providers/AnimationThemeProvider.tsx +62 -0
  35. package/src/molecules/animation/presentation/providers/__tests__/AnimationThemeProvider.test.tsx +165 -0
  36. package/src/molecules/animation/types/global.d.ts +97 -0
  37. package/src/molecules/celebration/domain/entities/CelebrationConfig.ts +17 -0
  38. package/src/molecules/celebration/domain/entities/FireworksConfig.ts +32 -0
  39. package/src/molecules/celebration/index.ts +93 -0
  40. package/src/molecules/celebration/infrastructure/services/FireworksConfigService.ts +49 -0
  41. package/src/molecules/celebration/presentation/components/CelebrationFireworksOverlay.tsx +33 -0
  42. package/src/molecules/celebration/presentation/components/CelebrationModal.tsx +78 -0
  43. package/src/molecules/celebration/presentation/components/CelebrationModalContent.tsx +90 -0
  44. package/src/molecules/celebration/presentation/hooks/useCelebrationModalAnimation.ts +49 -0
  45. package/src/molecules/celebration/presentation/hooks/useCelebrationState.ts +45 -0
  46. package/src/molecules/celebration/presentation/styles/CelebrationModalStyles.ts +65 -0
  47. package/src/molecules/countdown/components/Countdown.tsx +128 -0
  48. package/src/molecules/countdown/components/CountdownHeader.tsx +84 -0
  49. package/src/molecules/countdown/components/TimeUnit.tsx +73 -0
  50. package/src/molecules/countdown/hooks/useCountdown.ts +107 -0
  51. package/src/molecules/countdown/index.ts +25 -0
  52. package/src/molecules/countdown/types/CountdownTypes.ts +31 -0
  53. package/src/molecules/countdown/utils/TimeCalculator.ts +46 -0
  54. package/src/molecules/emoji/domain/entities/Emoji.ts +129 -0
  55. package/src/molecules/emoji/index.ts +177 -0
  56. package/src/molecules/emoji/presentation/components/EmojiPicker.tsx +102 -0
  57. package/src/molecules/emoji/presentation/hooks/useEmojiPicker.ts +171 -0
  58. package/src/molecules/index.ts +21 -0
  59. package/src/molecules/long-press-menu/domain/entities/MenuAction.ts +37 -0
  60. package/src/molecules/long-press-menu/index.ts +16 -0
  61. package/src/molecules/navigation/StackNavigator.tsx +75 -0
  62. package/src/molecules/navigation/TabsNavigator.tsx +94 -0
  63. package/src/molecules/navigation/components/FabButton.tsx +45 -0
  64. package/src/molecules/navigation/components/TabLabel.tsx +47 -0
  65. package/src/molecules/navigation/createStackNavigator.ts +20 -0
  66. package/src/molecules/navigation/createTabNavigator.ts +20 -0
  67. package/src/molecules/navigation/hooks/useTabBarStyles.ts +54 -0
  68. package/src/molecules/navigation/index.ts +37 -0
  69. package/src/molecules/navigation/types.ts +118 -0
  70. package/src/molecules/navigation/utils/AppNavigation.ts +101 -0
  71. package/src/molecules/navigation/utils/IconRenderer.ts +50 -0
  72. package/src/molecules/navigation/utils/LabelProcessor.ts +70 -0
  73. package/src/molecules/navigation/utils/NavigationCleanup.ts +62 -0
  74. package/src/molecules/navigation/utils/NavigationTheme.ts +21 -0
  75. package/src/molecules/navigation/utils/NavigationValidator.ts +61 -0
  76. package/src/molecules/navigation/utils/ScreenFactory.ts +115 -0
  77. package/src/molecules/navigation/utils/__tests__/IconRenderer.getIconName.test.ts +109 -0
  78. package/src/molecules/navigation/utils/__tests__/IconRenderer.renderIcon.test.ts +116 -0
  79. package/src/molecules/navigation/utils/__tests__/LabelProcessor.processLabel.test.ts +116 -0
  80. package/src/molecules/navigation/utils/__tests__/LabelProcessor.processTitle.test.ts +59 -0
  81. package/src/molecules/navigation/utils/__tests__/NavigationCleanup.test.ts +271 -0
  82. package/src/molecules/navigation/utils/__tests__/NavigationValidator.test.ts +252 -0
  83. package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +194 -0
  84. package/src/molecules/swipe-actions/index.ts +6 -0
  85. package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +131 -0
  86. package/src/theme/hooks/useResponsiveDesignTokens.ts +1 -1
  87. package/src/utilities/clipboard/ClipboardUtils.ts +71 -0
  88. package/src/utilities/clipboard/index.ts +5 -0
  89. package/src/utilities/index.ts +6 -0
  90. package/src/utilities/sharing/domain/entities/Share.ts +210 -0
  91. package/src/utilities/sharing/index.ts +205 -0
  92. package/src/utilities/sharing/infrastructure/services/SharingService.ts +165 -0
  93. package/src/utilities/sharing/presentation/hooks/useSharing.ts +154 -0
@@ -0,0 +1,59 @@
1
+ import { LabelProcessor } from '../LabelProcessor';
2
+
3
+ describe('LabelProcessor - processTitle', () => {
4
+ beforeEach(() => {
5
+ // Clear cache before each test
6
+ (LabelProcessor as any).labelCache.clear();
7
+ });
8
+
9
+ it('should return undefined when title is undefined', () => {
10
+ const result = LabelProcessor.processTitle(undefined);
11
+
12
+ expect(result).toBeUndefined();
13
+ });
14
+
15
+ it('should return undefined when title is null', () => {
16
+ const result = LabelProcessor.processTitle(null as any);
17
+
18
+ expect(result).toBeUndefined();
19
+ });
20
+
21
+ it('should return undefined when title is not a string', () => {
22
+ const result = LabelProcessor.processTitle(123 as any);
23
+
24
+ expect(result).toBeUndefined();
25
+ });
26
+
27
+ it('should return original title when no getLabel function provided', () => {
28
+ const result = LabelProcessor.processTitle('Test Title');
29
+
30
+ expect(result).toBe('Test Title');
31
+ });
32
+
33
+ it('should process title using getLabel function', () => {
34
+ const getLabel = jest.fn((label: string) => label.toUpperCase());
35
+
36
+ const result = LabelProcessor.processTitle('test title', getLabel);
37
+
38
+ expect(result).toBe('TEST TITLE');
39
+ expect(getLabel).toHaveBeenCalledWith('test title');
40
+ });
41
+
42
+ it('should handle empty string title', () => {
43
+ const getLabel = jest.fn((label: string) => label.toUpperCase());
44
+
45
+ const result = LabelProcessor.processTitle('', getLabel);
46
+
47
+ expect(result).toBe('');
48
+ expect(getLabel).toHaveBeenCalledWith('');
49
+ });
50
+
51
+ it('should handle special characters in title', () => {
52
+ const getLabel = jest.fn((label: string) => label);
53
+
54
+ const result = LabelProcessor.processTitle('Test & Title (Special)', getLabel);
55
+
56
+ expect(result).toBe('Test & Title (Special)');
57
+ expect(getLabel).toHaveBeenCalledWith('Test & Title (Special)');
58
+ });
59
+ });
@@ -0,0 +1,271 @@
1
+ import { NavigationCleanupManager } from '../NavigationCleanup';
2
+
3
+ describe('NavigationCleanup', () => {
4
+ describe('NavigationCleanupManager', () => {
5
+ describe('createMultiCleanup', () => {
6
+ it('should create a cleanup function that calls all unsubscribers', () => {
7
+ const unsubscribe1 = jest.fn();
8
+ const unsubscribe2 = jest.fn();
9
+ const unsubscribe3 = jest.fn();
10
+
11
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsubscribe1, unsubscribe2, unsubscribe3]);
12
+
13
+ expect(typeof cleanup).toBe('function');
14
+
15
+ cleanup();
16
+
17
+ expect(unsubscribe1).toHaveBeenCalledTimes(1);
18
+ expect(unsubscribe2).toHaveBeenCalledTimes(1);
19
+ expect(unsubscribe3).toHaveBeenCalledTimes(1);
20
+ });
21
+
22
+ it('should handle empty array of unsubscribers', () => {
23
+ const cleanup = NavigationCleanupManager.createMultiCleanup([]);
24
+
25
+ expect(() => cleanup()).not.toThrow();
26
+ });
27
+
28
+ it('should handle single unsubscribe function', () => {
29
+ const unsubscribe = jest.fn();
30
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsubscribe]);
31
+
32
+ cleanup();
33
+
34
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
35
+ });
36
+
37
+ it('should handle errors in individual unsubscribe functions', () => {
38
+ const originalDev = __DEV__;
39
+ (global as any).__DEV__ = true;
40
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
41
+
42
+ const unsubscribe1 = jest.fn();
43
+ const unsubscribe2 = jest.fn(() => {
44
+ throw new Error('Unsubscribe failed');
45
+ });
46
+ const unsubscribe3 = jest.fn();
47
+
48
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsubscribe1, unsubscribe2, unsubscribe3]);
49
+
50
+ cleanup();
51
+
52
+ expect(unsubscribe1).toHaveBeenCalledTimes(1);
53
+ expect(unsubscribe2).toHaveBeenCalledTimes(1);
54
+ expect(unsubscribe3).toHaveBeenCalledTimes(1);
55
+ expect(consoleSpy).toHaveBeenCalledWith(
56
+ '[NavigationCleanupManager] Error during cleanup:',
57
+ expect.any(Error)
58
+ );
59
+
60
+ (global as any).__DEV__ = originalDev;
61
+ consoleSpy.mockRestore();
62
+ });
63
+
64
+ it('should not log errors in production', () => {
65
+ const originalDev = __DEV__;
66
+ (global as any).__DEV__ = false;
67
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
68
+
69
+ const unsubscribe = jest.fn(() => {
70
+ throw new Error('Unsubscribe failed');
71
+ });
72
+
73
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsubscribe]);
74
+
75
+ cleanup();
76
+
77
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
78
+ expect(consoleSpy).not.toHaveBeenCalled();
79
+
80
+ (global as any).__DEV__ = originalDev;
81
+ consoleSpy.mockRestore();
82
+ });
83
+
84
+ it('should handle multiple errors', () => {
85
+ const originalDev = __DEV__;
86
+ (global as any).__DEV__ = true;
87
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
88
+
89
+ const unsubscribe1 = jest.fn(() => {
90
+ throw new Error('Error 1');
91
+ });
92
+ const unsubscribe2 = jest.fn(() => {
93
+ throw new Error('Error 2');
94
+ });
95
+
96
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsubscribe1, unsubscribe2]);
97
+
98
+ cleanup();
99
+
100
+ expect(unsubscribe1).toHaveBeenCalledTimes(1);
101
+ expect(unsubscribe2).toHaveBeenCalledTimes(1);
102
+ expect(consoleSpy).toHaveBeenCalledTimes(2);
103
+
104
+ (global as any).__DEV__ = originalDev;
105
+ consoleSpy.mockRestore();
106
+ });
107
+ });
108
+
109
+ describe('safeCleanup', () => {
110
+ it('should create a safe cleanup wrapper', () => {
111
+ const unsubscribe = jest.fn();
112
+ const safeUnsubscribe = NavigationCleanupManager.safeCleanup(unsubscribe);
113
+
114
+ expect(typeof safeUnsubscribe).toBe('function');
115
+
116
+ safeUnsubscribe();
117
+
118
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
119
+ });
120
+
121
+ it('should handle errors in unsubscribe function', () => {
122
+ const originalDev = __DEV__;
123
+ (global as any).__DEV__ = true;
124
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
125
+
126
+ const unsubscribe = jest.fn(() => {
127
+ throw new Error('Unsubscribe failed');
128
+ });
129
+
130
+ const safeUnsubscribe = NavigationCleanupManager.safeCleanup(unsubscribe);
131
+
132
+ expect(() => safeUnsubscribe()).not.toThrow();
133
+
134
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
135
+ expect(consoleSpy).toHaveBeenCalledWith(
136
+ '[NavigationCleanupManager] Error during cleanup:',
137
+ expect.any(Error)
138
+ );
139
+
140
+ (global as any).__DEV__ = originalDev;
141
+ consoleSpy.mockRestore();
142
+ });
143
+
144
+ it('should not log errors in production', () => {
145
+ const originalDev = __DEV__;
146
+ (global as any).__DEV__ = false;
147
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
148
+
149
+ const unsubscribe = jest.fn(() => {
150
+ throw new Error('Unsubscribe failed');
151
+ });
152
+
153
+ const safeUnsubscribe = NavigationCleanupManager.safeCleanup(unsubscribe);
154
+
155
+ safeUnsubscribe();
156
+
157
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
158
+ expect(consoleSpy).not.toHaveBeenCalled();
159
+
160
+ (global as any).__DEV__ = originalDev;
161
+ consoleSpy.mockRestore();
162
+ });
163
+
164
+ it('should handle successful unsubscribe', () => {
165
+ const unsubscribe = jest.fn();
166
+ const safeUnsubscribe = NavigationCleanupManager.safeCleanup(unsubscribe);
167
+
168
+ safeUnsubscribe();
169
+
170
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
171
+ });
172
+
173
+ it('should handle unsubscribe that returns a value', () => {
174
+ const unsubscribe = jest.fn().mockReturnValue('cleanup result');
175
+ const safeUnsubscribe = NavigationCleanupManager.safeCleanup(unsubscribe);
176
+
177
+ const result = safeUnsubscribe();
178
+
179
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
180
+ expect(result).toBeUndefined(); // safeCleanup doesn't return the value
181
+ });
182
+ });
183
+ });
184
+
185
+ describe('integration scenarios', () => {
186
+ it('should work with nested cleanup functions', () => {
187
+ const innerUnsubscribe1 = jest.fn();
188
+ const innerUnsubscribe2 = jest.fn();
189
+ const outerUnsubscribe = jest.fn();
190
+
191
+ const innerCleanup = NavigationCleanupManager.createMultiCleanup([innerUnsubscribe1, innerUnsubscribe2]);
192
+ const outerCleanup = NavigationCleanupManager.createMultiCleanup([innerCleanup, outerUnsubscribe]);
193
+
194
+ outerCleanup();
195
+
196
+ expect(innerUnsubscribe1).toHaveBeenCalledTimes(1);
197
+ expect(innerUnsubscribe2).toHaveBeenCalledTimes(1);
198
+ expect(outerUnsubscribe).toHaveBeenCalledTimes(1);
199
+ });
200
+
201
+ it('should work with mixed safe and unsafe cleanup functions', () => {
202
+ const unsafeUnsubscribe = jest.fn();
203
+ const safeUnsubscribe = NavigationCleanupManager.safeCleanup(jest.fn());
204
+
205
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsafeUnsubscribe, safeUnsubscribe]);
206
+
207
+ cleanup();
208
+
209
+ expect(unsafeUnsubscribe).toHaveBeenCalledTimes(1);
210
+ });
211
+
212
+ it('should handle cleanup functions that throw different types of errors', () => {
213
+ const originalDev = __DEV__;
214
+ (global as any).__DEV__ = true;
215
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
216
+
217
+ const stringError = jest.fn(() => {
218
+ throw 'String error';
219
+ });
220
+ const objectError = jest.fn(() => {
221
+ throw { code: 500, message: 'Object error' };
222
+ });
223
+ const errorInstance = jest.fn(() => {
224
+ throw new Error('Error instance');
225
+ });
226
+
227
+ const cleanup = NavigationCleanupManager.createMultiCleanup([stringError, objectError, errorInstance]);
228
+
229
+ cleanup();
230
+
231
+ expect(stringError).toHaveBeenCalledTimes(1);
232
+ expect(objectError).toHaveBeenCalledTimes(1);
233
+ expect(errorInstance).toHaveBeenCalledTimes(1);
234
+ expect(consoleSpy).toHaveBeenCalledTimes(3);
235
+
236
+ (global as any).__DEV__ = originalDev;
237
+ consoleSpy.mockRestore();
238
+ });
239
+ });
240
+
241
+ describe('edge cases', () => {
242
+ it('should handle null/undefined in unsubscribe array', () => {
243
+ const unsubscribe = jest.fn();
244
+
245
+ expect(() => {
246
+ const cleanup = NavigationCleanupManager.createMultiCleanup([unsubscribe, null, undefined] as any);
247
+ cleanup();
248
+ }).not.toThrow();
249
+
250
+ expect(unsubscribe).toHaveBeenCalledTimes(1);
251
+ });
252
+
253
+ it('should handle non-function unsubscribe', () => {
254
+ const originalDev = __DEV__;
255
+ (global as any).__DEV__ = true;
256
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
257
+
258
+ const validUnsubscribe = jest.fn();
259
+
260
+ expect(() => {
261
+ const cleanup = NavigationCleanupManager.createMultiCleanup([validUnsubscribe, 'not-a-function'] as any);
262
+ cleanup();
263
+ }).not.toThrow();
264
+
265
+ expect(validUnsubscribe).toHaveBeenCalledTimes(1);
266
+
267
+ (global as any).__DEV__ = originalDev;
268
+ consoleSpy.mockRestore();
269
+ });
270
+ });
271
+ });
@@ -0,0 +1,252 @@
1
+ import { NavigationValidator } from '../NavigationValidator';
2
+ import type { TabScreen, StackScreen } from '../../types';
3
+
4
+ describe('NavigationValidator', () => {
5
+ const mockTabScreens: TabScreen[] = [
6
+ {
7
+ name: 'Home',
8
+ component: () => null,
9
+ label: 'Home',
10
+ icon: 'home',
11
+ },
12
+ {
13
+ name: 'Profile',
14
+ component: () => null,
15
+ label: 'Profile',
16
+ icon: 'profile',
17
+ },
18
+ ];
19
+
20
+ const mockStackScreens: StackScreen[] = [
21
+ {
22
+ name: 'Home',
23
+ component: () => null,
24
+ },
25
+ {
26
+ name: 'Profile',
27
+ component: () => null,
28
+ },
29
+ ];
30
+
31
+ describe('validateScreens', () => {
32
+ describe('tab navigator validation', () => {
33
+ it('should validate valid tab screens', () => {
34
+ expect(() => {
35
+ NavigationValidator.validateScreens(mockTabScreens, 'tab');
36
+ }).not.toThrow();
37
+ });
38
+
39
+ it('should throw error for non-array screens', () => {
40
+ expect(() => {
41
+ NavigationValidator.validateScreens({} as any, 'tab');
42
+ }).toThrow('Screens must be an array for tab navigator');
43
+ });
44
+
45
+ it('should warn for empty screens array', () => {
46
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
47
+
48
+ NavigationValidator.validateScreens([], 'tab');
49
+
50
+ expect(consoleSpy).toHaveBeenCalledWith('[NavigationValidator] No screens provided for tab navigator');
51
+ consoleSpy.mockRestore();
52
+ });
53
+
54
+ it('should throw error for screen without name', () => {
55
+ const invalidScreens = [{ component: () => null, label: 'Test', icon: 'test' }] as any;
56
+
57
+ expect(() => {
58
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
59
+ }).toThrow('Screen at index 0 must have a valid non-empty name');
60
+ });
61
+
62
+ it('should throw error for screen with empty name', () => {
63
+ const invalidScreens = [{ name: '', component: () => null, label: 'Test', icon: 'test' }] as any;
64
+
65
+ expect(() => {
66
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
67
+ }).toThrow('Screen at index 0 must have a valid non-empty name');
68
+ });
69
+
70
+ it('should throw error for screen without component', () => {
71
+ const invalidScreens = [{ name: 'Test', label: 'Test', icon: 'test' }] as any;
72
+
73
+ expect(() => {
74
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
75
+ }).toThrow('Screen \'Test\' must have a valid component');
76
+ });
77
+
78
+ it('should throw error for screen with invalid component', () => {
79
+ const invalidScreens = [{ name: 'Test', component: 'not-a-function', label: 'Test', icon: 'test' }] as any;
80
+
81
+ expect(() => {
82
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
83
+ }).toThrow('Screen \'Test\' must have a valid component');
84
+ });
85
+
86
+ it('should throw error for tab screen without label', () => {
87
+ const invalidScreens = [{ name: 'Test', component: () => null, icon: 'test' }] as any;
88
+
89
+ expect(() => {
90
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
91
+ }).toThrow('Tab screen \'Test\' must have a valid non-empty label');
92
+ });
93
+
94
+ it('should throw error for tab screen with empty label', () => {
95
+ const invalidScreens = [{ name: 'Test', component: () => null, label: '', icon: 'test' }] as any;
96
+
97
+ expect(() => {
98
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
99
+ }).toThrow('Tab screen \'Test\' must have a valid non-empty label');
100
+ });
101
+
102
+ it('should warn for tab screen with invalid icon', () => {
103
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
104
+ const screensWithInvalidIcon = [{ name: 'Test', component: () => null, label: 'Test', icon: '' }] as any;
105
+
106
+ NavigationValidator.validateScreens(screensWithInvalidIcon, 'tab');
107
+
108
+ expect(consoleSpy).toHaveBeenCalledWith('[NavigationValidator] Tab screen \'Test\' has invalid icon, it will be ignored');
109
+ consoleSpy.mockRestore();
110
+ });
111
+
112
+ it('should throw error for tab screen with too long label', () => {
113
+ const longLabel = 'a'.repeat(51);
114
+ const invalidScreens = [{ name: 'Test', component: () => null, label: longLabel, icon: 'test' }] as any;
115
+
116
+ expect(() => {
117
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
118
+ }).toThrow('Tab screen \'Test\' label too long (max 50 characters)');
119
+ });
120
+
121
+ it('should throw error for duplicate screen names', () => {
122
+ const duplicateScreens = [
123
+ { name: 'Home', component: () => null, label: 'Home', icon: 'home' },
124
+ { name: 'Home', component: () => null, label: 'Home 2', icon: 'home2' },
125
+ ] as any;
126
+
127
+ expect(() => {
128
+ NavigationValidator.validateScreens(duplicateScreens, 'tab');
129
+ }).toThrow('Duplicate screen name \'Home\' found at index 1');
130
+ });
131
+ });
132
+
133
+ describe('stack navigator validation', () => {
134
+ it('should validate valid stack screens', () => {
135
+ expect(() => {
136
+ NavigationValidator.validateScreens(mockStackScreens, 'stack');
137
+ }).not.toThrow();
138
+ });
139
+
140
+ it('should throw error for stack screen without name', () => {
141
+ const invalidScreens = [{ component: () => null }] as any;
142
+
143
+ expect(() => {
144
+ NavigationValidator.validateScreens(invalidScreens, 'stack');
145
+ }).toThrow('Screen at index 0 must have a valid non-empty name');
146
+ });
147
+
148
+ it('should throw error for stack screen without component', () => {
149
+ const invalidScreens = [{ name: 'Test' }] as any;
150
+
151
+ expect(() => {
152
+ NavigationValidator.validateScreens(invalidScreens, 'stack');
153
+ }).toThrow('Screen \'Test\' must have a valid component');
154
+ });
155
+
156
+ it('should throw error for duplicate stack screen names', () => {
157
+ const duplicateScreens = [
158
+ { name: 'Home', component: () => null },
159
+ { name: 'Home', component: () => null },
160
+ ] as any;
161
+
162
+ expect(() => {
163
+ NavigationValidator.validateScreens(duplicateScreens, 'stack');
164
+ }).toThrow('Duplicate screen name \'Home\' found at index 1');
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('validateInitialRoute', () => {
170
+ it('should not throw for valid initial route', () => {
171
+ expect(() => {
172
+ NavigationValidator.validateInitialRoute('Home', mockTabScreens);
173
+ }).not.toThrow();
174
+ });
175
+
176
+ it('should not throw for undefined initial route', () => {
177
+ expect(() => {
178
+ NavigationValidator.validateInitialRoute(undefined, mockTabScreens);
179
+ }).not.toThrow();
180
+ });
181
+
182
+ it('should throw error for initial route not in screens', () => {
183
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
184
+
185
+ expect(() => {
186
+ NavigationValidator.validateInitialRoute('NonExistent', mockTabScreens);
187
+ }).toThrow('Initial route \'NonExistent\' not found in screens. Available screens: Home, Profile');
188
+
189
+ consoleSpy.mockRestore();
190
+ });
191
+
192
+ it('should log error in development for invalid initial route', () => {
193
+ const originalDev = __DEV__;
194
+ (global as any).__DEV__ = true;
195
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
196
+
197
+ try {
198
+ NavigationValidator.validateInitialRoute('NonExistent', mockTabScreens);
199
+ } catch (_error) {
200
+ // Expected to throw
201
+ }
202
+
203
+ expect(consoleSpy).toHaveBeenCalledWith(
204
+ '[NavigationValidator] Initial route \'NonExistent\' not found in screens. Available screens: Home, Profile'
205
+ );
206
+
207
+ (global as any).__DEV__ = originalDev;
208
+ consoleSpy.mockRestore();
209
+ });
210
+
211
+ it('should handle empty screens array', () => {
212
+ expect(() => {
213
+ NavigationValidator.validateInitialRoute('Home', []);
214
+ }).toThrow('Initial route \'Home\' not found in screens. Available screens: ');
215
+ });
216
+
217
+ it('should work with both tab and stack screens', () => {
218
+ expect(() => {
219
+ NavigationValidator.validateInitialRoute('Home', mockTabScreens);
220
+ NavigationValidator.validateInitialRoute('Profile', mockStackScreens);
221
+ }).not.toThrow();
222
+ });
223
+ });
224
+
225
+ describe('edge cases', () => {
226
+ it('should handle screens with whitespace-only names', () => {
227
+ const invalidScreens = [{ name: ' ', component: () => null, label: 'Test', icon: 'test' }] as any;
228
+
229
+ expect(() => {
230
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
231
+ }).toThrow('Screen at index 0 must have a valid non-empty name');
232
+ });
233
+
234
+ it('should handle screens with whitespace-only labels', () => {
235
+ const invalidScreens = [{ name: 'Test', component: () => null, label: ' ', icon: 'test' }] as any;
236
+
237
+ expect(() => {
238
+ NavigationValidator.validateScreens(invalidScreens, 'tab');
239
+ }).toThrow('Tab screen \'Test\' must have a valid non-empty label');
240
+ });
241
+
242
+ it('should handle null/undefined values gracefully', () => {
243
+ expect(() => {
244
+ NavigationValidator.validateScreens(null as any, 'tab');
245
+ }).toThrow('Screens must be an array for tab navigator');
246
+
247
+ expect(() => {
248
+ NavigationValidator.validateScreens(undefined as any, 'tab');
249
+ }).toThrow('Screens must be an array for tab navigator');
250
+ });
251
+ });
252
+ });