@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,70 @@
1
+ import type { LabelProcessorProps } from "../types";
2
+
3
+ export class LabelProcessor {
4
+ private static labelCache = new Map<string, string>();
5
+ private static maxCacheSize = 50;
6
+ private static cacheHits = 0;
7
+ private static cacheMisses = 0;
8
+
9
+ static processLabel(props: LabelProcessorProps): string {
10
+ const { label, getLabel } = props;
11
+
12
+ // Create stable cache key
13
+ const getLabelKey = getLabel ? getLabel.toString().slice(0, 20) : 'none';
14
+ const cacheKey = `${String(label)}-${getLabelKey}`;
15
+
16
+ // Check cache first
17
+ if (this.labelCache.has(cacheKey)) {
18
+ this.cacheHits++;
19
+ return this.labelCache.get(cacheKey)!;
20
+ }
21
+
22
+ this.cacheMisses++;
23
+ let result: string;
24
+
25
+ if (!getLabel) {
26
+ result = label;
27
+ } else {
28
+ try {
29
+ const processedLabel = getLabel(label);
30
+ result = typeof processedLabel === "string" ? processedLabel : label;
31
+ } catch (error) {
32
+ if (__DEV__) {
33
+ console.error(`[LabelProcessor] Error processing label: ${label}`, error);
34
+ }
35
+ result = label;
36
+ }
37
+ }
38
+
39
+ // Cache management with LRU eviction
40
+ if (this.labelCache.size >= this.maxCacheSize) {
41
+ const firstKey = this.labelCache.keys().next().value;
42
+ if (firstKey) {
43
+ this.labelCache.delete(firstKey);
44
+ }
45
+ }
46
+ this.labelCache.set(cacheKey, result);
47
+
48
+ // Log cache performance in development
49
+ if (__DEV__ && (this.cacheHits + this.cacheMisses) % 100 === 0) {
50
+ const hitRate = (this.cacheHits / (this.cacheHits + this.cacheMisses)) * 100;
51
+ console.log(`[LabelProcessor] Cache hit rate: ${hitRate.toFixed(1)}%`);
52
+ }
53
+
54
+ return result;
55
+ }
56
+
57
+ static clearCache(): void {
58
+ this.labelCache.clear();
59
+ this.cacheHits = 0;
60
+ this.cacheMisses = 0;
61
+ }
62
+
63
+ static processTitle(title: string | undefined, getLabel?: (label: string) => string): string | undefined {
64
+ if (title === undefined || typeof title !== "string") {
65
+ return undefined;
66
+ }
67
+
68
+ return getLabel ? getLabel(title) : title;
69
+ }
70
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Navigation Event Cleanup Utilities
3
+ *
4
+ * This file provides patterns for proper cleanup of navigation event listeners
5
+ * to prevent memory leaks in React Native applications.
6
+ *
7
+ * Example usage:
8
+ * ```tsx
9
+ * useEffect(() => {
10
+ * const unsubscribe = navigation.addListener('focus', handleFocus);
11
+ * return unsubscribe; // Automatic cleanup on unmount
12
+ * }, [navigation, handleFocus]);
13
+ *
14
+ * // For multiple listeners:
15
+ * useEffect(() => {
16
+ * const unsubscribers = [
17
+ * navigation.addListener('focus', handleFocus),
18
+ * navigation.addListener('blur', handleBlur),
19
+ * ];
20
+ *
21
+ * return () => unsubscribers.forEach(unsubscribe => unsubscribe());
22
+ * }, [navigation, handleFocus, handleBlur]);
23
+ * ```
24
+ */
25
+
26
+ export interface NavigationCleanup {
27
+ unsubscribe: () => void;
28
+ }
29
+
30
+ export class NavigationCleanupManager {
31
+ /**
32
+ * Creates a cleanup function for multiple navigation listeners
33
+ */
34
+ static createMultiCleanup(unsubscribers: (() => void)[]): () => void {
35
+ return () => {
36
+ unsubscribers.forEach(unsubscribe => {
37
+ try {
38
+ unsubscribe();
39
+ } catch (error) {
40
+ if (__DEV__) {
41
+ console.warn('[NavigationCleanupManager] Error during cleanup:', error);
42
+ }
43
+ }
44
+ });
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Safe cleanup wrapper that handles errors gracefully
50
+ */
51
+ static safeCleanup(unsubscribe: () => void): () => void {
52
+ return () => {
53
+ try {
54
+ unsubscribe();
55
+ } catch (error) {
56
+ if (__DEV__) {
57
+ console.warn('[NavigationCleanupManager] Error during cleanup:', error);
58
+ }
59
+ }
60
+ };
61
+ }
62
+ }
@@ -0,0 +1,21 @@
1
+ import { DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme } from '@react-navigation/native';
2
+
3
+ /**
4
+ * Create a navigation theme based on design tokens and mode
5
+ */
6
+ export const createNavigationTheme = (colors: any, mode: 'light' | 'dark'): Theme => {
7
+ const baseTheme = mode === 'dark' ? NavigationDarkTheme : NavigationDefaultTheme;
8
+
9
+ return {
10
+ ...baseTheme,
11
+ colors: {
12
+ ...baseTheme.colors,
13
+ primary: colors.primary,
14
+ background: colors.backgroundPrimary,
15
+ card: colors.surface,
16
+ text: colors.textPrimary,
17
+ border: colors.border,
18
+ notification: colors.error,
19
+ },
20
+ };
21
+ };
@@ -0,0 +1,61 @@
1
+ import type { TabScreen, StackScreen } from "../types";
2
+
3
+ export class NavigationValidator {
4
+ static validateScreens(screens: TabScreen[] | StackScreen[], type: "tab" | "stack"): void {
5
+ if (!Array.isArray(screens)) {
6
+ throw new Error(`Screens must be an array for ${type} navigator`);
7
+ }
8
+
9
+ if (screens.length === 0) {
10
+ if (__DEV__) {
11
+ console.warn(`[NavigationValidator] No screens provided for ${type} navigator`);
12
+ }
13
+ return;
14
+ }
15
+
16
+ const screenNames = new Set<string>();
17
+
18
+ screens.forEach((screen, index) => {
19
+ if (!screen.name || typeof screen.name !== "string" || screen.name.trim() === "") {
20
+ throw new Error(`Screen at index ${index} must have a valid non-empty name`);
21
+ }
22
+
23
+ // Check for duplicate screen names
24
+ if (screenNames.has(screen.name)) {
25
+ throw new Error(`Duplicate screen name '${screen.name}' found at index ${index}`);
26
+ }
27
+ screenNames.add(screen.name);
28
+
29
+ if (!screen.component || typeof screen.component !== "function") {
30
+ throw new Error(`Screen '${screen.name}' must have a valid component`);
31
+ }
32
+
33
+ if (type === "tab") {
34
+ const tabScreen = screen as TabScreen;
35
+ if (!tabScreen.label || typeof tabScreen.label !== "string" || tabScreen.label.trim() === "") {
36
+ throw new Error(`Tab screen '${screen.name}' must have a valid non-empty label`);
37
+ }
38
+
39
+ if (tabScreen.icon !== undefined && (typeof tabScreen.icon !== "string" || tabScreen.icon.trim() === "")) {
40
+ if (__DEV__) {
41
+ console.warn(`[NavigationValidator] Tab screen '${screen.name}' has invalid icon, it will be ignored`);
42
+ }
43
+ }
44
+
45
+ if (tabScreen.label.length > 50) {
46
+ throw new Error(`Tab screen '${screen.name}' label too long (max 50 characters)`);
47
+ }
48
+ }
49
+ });
50
+ }
51
+
52
+ static validateInitialRoute(initialRouteName: string | undefined, screens: TabScreen[] | StackScreen[]): void {
53
+ if (initialRouteName && !screens.find(screen => screen.name === initialRouteName)) {
54
+ const error = `Initial route '${initialRouteName}' not found in screens. Available screens: ${screens.map(s => s.name).join(", ")}`;
55
+ if (__DEV__) {
56
+ console.error(`[NavigationValidator] ${error}`);
57
+ }
58
+ throw new Error(error);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,115 @@
1
+ import React from "react";
2
+ import type { ParamListBase } from "@react-navigation/native";
3
+ import type {
4
+ BottomTabScreenProps,
5
+ BottomTabNavigationOptions,
6
+ } from "@react-navigation/bottom-tabs";
7
+ import type {
8
+ TabScreen,
9
+ StackScreen,
10
+ TabNavigatorConfig,
11
+ StackNavigatorConfig,
12
+ } from "../types";
13
+ import { LabelProcessor } from "./LabelProcessor";
14
+ import { IconRenderer } from "./IconRenderer";
15
+
16
+
17
+
18
+ export function createTabScreen<T extends ParamListBase = ParamListBase>(
19
+ screen: TabScreen<T>,
20
+ config: TabNavigatorConfig<T>,
21
+ Tab: any
22
+ ): React.ReactElement {
23
+ const screenOptions = (
24
+ props: BottomTabScreenProps<T>
25
+ ): BottomTabNavigationOptions => {
26
+ const processedLabel = LabelProcessor.processLabel({
27
+ label: screen.label,
28
+ getLabel: config.getLabel,
29
+ });
30
+
31
+ const isFab = screen.isFab ?? false;
32
+
33
+ const baseOptions: BottomTabNavigationOptions = {
34
+ tabBarLabel: processedLabel,
35
+ title: processedLabel,
36
+ tabBarIcon: ({ focused }: { focused: boolean }) => {
37
+ const iconName = IconRenderer.getIconName(
38
+ screen.name,
39
+ focused,
40
+ screen.icon ?? "",
41
+ config.getTabIcon
42
+ );
43
+
44
+ return IconRenderer.renderIcon(
45
+ { iconName, focused, routeName: screen.name, isFab },
46
+ config.renderIcon
47
+ );
48
+ },
49
+ };
50
+
51
+ if (screen.options) {
52
+ if (typeof screen.options === "function") {
53
+ return {
54
+ ...baseOptions,
55
+ ...screen.options(props),
56
+ };
57
+ }
58
+ return {
59
+ ...baseOptions,
60
+ ...screen.options,
61
+ };
62
+ }
63
+
64
+ return baseOptions;
65
+ };
66
+
67
+ return React.createElement(Tab.Screen, {
68
+ key: screen.name,
69
+ name: screen.name,
70
+ component: screen.component,
71
+ options: screenOptions,
72
+ });
73
+ }
74
+
75
+ export function createStackScreen<T extends ParamListBase = ParamListBase>(
76
+ screen: StackScreen<T>,
77
+ config: StackNavigatorConfig<T>,
78
+ Stack: any
79
+ ): React.ReactElement {
80
+ const screenOptions = (props: { navigation: unknown; route: unknown }) => {
81
+ if (screen.options) {
82
+ if (typeof screen.options === "function") {
83
+ const customOptions = screen.options(props);
84
+ const processedTitle = LabelProcessor.processTitle(
85
+ customOptions.title,
86
+ config.getLabel
87
+ );
88
+
89
+ return {
90
+ ...customOptions,
91
+ ...(processedTitle && { title: processedTitle }),
92
+ };
93
+ }
94
+
95
+ const processedTitle = LabelProcessor.processTitle(
96
+ screen.options.title,
97
+ config.getLabel
98
+ );
99
+
100
+ return {
101
+ ...screen.options,
102
+ ...(processedTitle && { title: processedTitle }),
103
+ };
104
+ }
105
+
106
+ return {};
107
+ };
108
+
109
+ return React.createElement(Stack.Screen, {
110
+ key: screen.name,
111
+ name: screen.name,
112
+ component: screen.component,
113
+ options: screenOptions,
114
+ });
115
+ }
@@ -0,0 +1,109 @@
1
+ import { IconRenderer } from '../IconRenderer';
2
+
3
+ describe('IconRenderer - getIconName', () => {
4
+ it('should return original icon when no getTabIcon function provided', () => {
5
+ const result = IconRenderer.getIconName(
6
+ 'Home',
7
+ true,
8
+ 'home',
9
+ undefined
10
+ );
11
+
12
+ expect(result).toBe('home');
13
+ });
14
+
15
+ it('should get icon name using getTabIcon function', () => {
16
+ const getTabIcon = jest.fn((routeName, focused) =>
17
+ focused ? `${routeName}-active` : `${routeName}-inactive`
18
+ );
19
+
20
+ const result = IconRenderer.getIconName(
21
+ 'Home',
22
+ true,
23
+ 'home',
24
+ getTabIcon
25
+ );
26
+
27
+ expect(result).toBe('Home-active');
28
+ expect(getTabIcon).toHaveBeenCalledWith('Home', true);
29
+ });
30
+
31
+ it('should call getTabIcon when provided', () => {
32
+ const getTabIcon = jest.fn((routeName, focused) =>
33
+ `${routeName}-${focused ? 'active' : 'inactive'}`
34
+ );
35
+
36
+ IconRenderer.getIconName(
37
+ 'Home',
38
+ true,
39
+ 'home',
40
+ getTabIcon
41
+ );
42
+
43
+ expect(getTabIcon).toHaveBeenCalledWith('Home', true);
44
+ });
45
+
46
+ it('should handle different focus states', () => {
47
+ const getTabIcon = jest.fn((routeName, focused) =>
48
+ `${routeName}-${focused ? 'focused' : 'unfocused'}`
49
+ );
50
+
51
+ const focusedResult = IconRenderer.getIconName(
52
+ 'Home',
53
+ true,
54
+ 'home',
55
+ getTabIcon
56
+ );
57
+
58
+ const unfocusedResult = IconRenderer.getIconName(
59
+ 'Home',
60
+ false,
61
+ 'home',
62
+ getTabIcon
63
+ );
64
+
65
+ expect(focusedResult).toBe('Home-focused');
66
+ expect(unfocusedResult).toBe('Home-unfocused');
67
+ expect(getTabIcon).toHaveBeenCalledTimes(2);
68
+ });
69
+
70
+ it('should handle different route names', () => {
71
+ const getTabIcon = jest.fn((routeName, focused) =>
72
+ `${routeName}-${focused ? 'on' : 'off'}`
73
+ );
74
+
75
+ const profileResult = IconRenderer.getIconName(
76
+ 'Profile',
77
+ true,
78
+ 'profile',
79
+ getTabIcon
80
+ );
81
+
82
+ const settingsResult = IconRenderer.getIconName(
83
+ 'Settings',
84
+ false,
85
+ 'settings',
86
+ getTabIcon
87
+ );
88
+
89
+ expect(profileResult).toBe('Profile-on');
90
+ expect(settingsResult).toBe('Settings-off');
91
+ expect(getTabIcon).toHaveBeenCalledTimes(2);
92
+ });
93
+
94
+ it('should handle original icon parameter correctly', () => {
95
+ const getTabIcon = jest.fn((routeName, focused) =>
96
+ `${routeName}-${focused ? 'active' : 'inactive'}`
97
+ );
98
+
99
+ const result = IconRenderer.getIconName(
100
+ 'Home',
101
+ true,
102
+ 'home-icon',
103
+ getTabIcon
104
+ );
105
+
106
+ expect(result).toBe('Home-active');
107
+ expect(getTabIcon).toHaveBeenCalledWith('Home', true);
108
+ });
109
+ });
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ import { IconRenderer } from "../IconRenderer";
3
+
4
+ describe("IconRenderer - renderIcon", () => {
5
+ it("should return null when no renderIcon function provided", () => {
6
+ const result = IconRenderer.renderIcon({
7
+ iconName: "home",
8
+ focused: true,
9
+ routeName: "Home",
10
+ isFab: false,
11
+ });
12
+
13
+ expect(result).toBeNull();
14
+ });
15
+
16
+ it("should render icon using renderIcon function", () => {
17
+ const MockIcon = () => React.createElement("View", { testID: "mock-icon" });
18
+ const renderIcon = jest.fn(() => React.createElement(MockIcon));
19
+
20
+ const result = IconRenderer.renderIcon(
21
+ {
22
+ iconName: "home",
23
+ focused: true,
24
+ routeName: "Home",
25
+ isFab: false,
26
+ },
27
+ renderIcon
28
+ );
29
+
30
+ expect(renderIcon).toHaveBeenCalledWith("home", true, "Home", false);
31
+ expect(result).toBeTruthy();
32
+ });
33
+
34
+ it("should return null when renderIcon throws error", () => {
35
+ const renderIcon = jest.fn(() => {
36
+ throw new Error("Icon render failed");
37
+ });
38
+
39
+ const result = IconRenderer.renderIcon(
40
+ {
41
+ iconName: "home",
42
+ focused: true,
43
+ routeName: "Home",
44
+ isFab: false,
45
+ },
46
+ renderIcon
47
+ );
48
+
49
+ expect(result).toBeNull();
50
+ });
51
+
52
+ it("should render icon successfully", () => {
53
+ const renderIcon = jest.fn(() => React.createElement("View"));
54
+
55
+ const result = IconRenderer.renderIcon(
56
+ {
57
+ iconName: "home",
58
+ focused: true,
59
+ routeName: "Home",
60
+ isFab: false,
61
+ },
62
+ renderIcon
63
+ );
64
+
65
+ expect(renderIcon).toHaveBeenCalledWith("home", true, "Home", false);
66
+ expect(result).toBeTruthy();
67
+ });
68
+
69
+ it("should pass correct parameters to renderIcon function", () => {
70
+ const renderIcon = jest.fn(() => React.createElement("View"));
71
+
72
+ IconRenderer.renderIcon(
73
+ {
74
+ iconName: "profile",
75
+ focused: false,
76
+ routeName: "Profile",
77
+ isFab: false,
78
+ },
79
+ renderIcon
80
+ );
81
+
82
+ expect(renderIcon).toHaveBeenCalledWith("profile", false, "Profile", false);
83
+ });
84
+
85
+ it("should pass isFab true for FAB tabs", () => {
86
+ const renderIcon = jest.fn(() => React.createElement("View"));
87
+
88
+ IconRenderer.renderIcon(
89
+ {
90
+ iconName: "zap",
91
+ focused: true,
92
+ routeName: "Generate",
93
+ isFab: true,
94
+ },
95
+ renderIcon
96
+ );
97
+
98
+ expect(renderIcon).toHaveBeenCalledWith("zap", true, "Generate", true);
99
+ });
100
+
101
+ it("should handle different icon names with isFab", () => {
102
+ const renderIcon = jest.fn(() => React.createElement("View"));
103
+
104
+ IconRenderer.renderIcon(
105
+ {
106
+ iconName: "settings",
107
+ focused: true,
108
+ routeName: "Settings",
109
+ isFab: false,
110
+ },
111
+ renderIcon
112
+ );
113
+
114
+ expect(renderIcon).toHaveBeenCalledWith("settings", true, "Settings", false);
115
+ });
116
+ });
@@ -0,0 +1,116 @@
1
+ import { LabelProcessor } from '../LabelProcessor';
2
+
3
+ describe('LabelProcessor - processLabel', () => {
4
+ beforeEach(() => {
5
+ // Clear cache before each test
6
+ (LabelProcessor as any).labelCache.clear();
7
+ });
8
+
9
+ it('should return original label when no getLabel function provided', () => {
10
+ const result = LabelProcessor.processLabel({
11
+ label: 'Test Label',
12
+ });
13
+
14
+ expect(result).toBe('Test Label');
15
+ });
16
+
17
+ it('should process label using getLabel function', () => {
18
+ const getLabel = jest.fn((label: string) => label.toUpperCase());
19
+
20
+ const result = LabelProcessor.processLabel({
21
+ label: 'test label',
22
+ getLabel,
23
+ });
24
+
25
+ expect(result).toBe('TEST LABEL');
26
+ expect(getLabel).toHaveBeenCalledWith('test label');
27
+ });
28
+
29
+ it('should return original label when getLabel returns non-string', () => {
30
+ const getLabel = jest.fn(() => 123);
31
+
32
+ const result = LabelProcessor.processLabel({
33
+ label: 'Test Label',
34
+ getLabel,
35
+ });
36
+
37
+ expect(result).toBe('Test Label');
38
+ });
39
+
40
+ it('should return original label when getLabel throws error', () => {
41
+ const getLabel = jest.fn(() => {
42
+ throw new Error('Processing failed');
43
+ });
44
+
45
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
46
+
47
+ const result = LabelProcessor.processLabel({
48
+ label: 'Test Label',
49
+ getLabel,
50
+ });
51
+
52
+ expect(result).toBe('Test Label');
53
+ expect(consoleSpy).toHaveBeenCalledWith(
54
+ '[LabelProcessor] Error processing label: Test Label',
55
+ expect.any(Error)
56
+ );
57
+
58
+ consoleSpy.mockRestore();
59
+ });
60
+
61
+ it('should cache results to improve performance', () => {
62
+ const getLabel = jest.fn((label: string) => label.toUpperCase());
63
+
64
+ // First call
65
+ const result1 = LabelProcessor.processLabel({
66
+ label: 'Test Label',
67
+ getLabel,
68
+ });
69
+
70
+ // Second call with same parameters
71
+ const result2 = LabelProcessor.processLabel({
72
+ label: 'Test Label',
73
+ getLabel,
74
+ });
75
+
76
+ expect(result1).toBe('TEST LABEL');
77
+ expect(result2).toBe('TEST LABEL');
78
+ expect(getLabel).toHaveBeenCalledTimes(1); // Should only be called once due to caching
79
+ });
80
+
81
+ it('should handle cache size limit to prevent memory leaks', () => {
82
+ const getLabel = jest.fn((label: string) => label.toUpperCase());
83
+
84
+ // Fill cache beyond limit
85
+ for (let i = 0; i < 150; i++) {
86
+ LabelProcessor.processLabel({
87
+ label: `label${i}`,
88
+ getLabel,
89
+ });
90
+ }
91
+
92
+ // Cache should not grow indefinitely
93
+ const cacheSize = (LabelProcessor as any).labelCache.size;
94
+ expect(cacheSize).toBeLessThanOrEqual(100);
95
+ });
96
+
97
+ it('should handle different getLabel functions separately', () => {
98
+ const getLabel1 = jest.fn((label: string) => label.toUpperCase());
99
+ const getLabel2 = jest.fn((label: string) => label.toLowerCase());
100
+
101
+ const result1 = LabelProcessor.processLabel({
102
+ label: 'Test Label 1',
103
+ getLabel: getLabel1,
104
+ });
105
+
106
+ const result2 = LabelProcessor.processLabel({
107
+ label: 'Test Label 2',
108
+ getLabel: getLabel2,
109
+ });
110
+
111
+ expect(result1).toBe('TEST LABEL 1');
112
+ expect(result2).toBe('test label 2');
113
+ expect(getLabel1).toHaveBeenCalledTimes(1);
114
+ expect(getLabel2).toHaveBeenCalledTimes(1);
115
+ });
116
+ });