@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.
- package/package.json +19 -2
- package/src/index.ts +105 -0
- package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +2 -2
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +1 -1
- package/src/molecules/animation/core/AnimationCore.ts +29 -0
- package/src/molecules/animation/domain/entities/Animation.ts +81 -0
- package/src/molecules/animation/domain/entities/Fireworks.ts +44 -0
- package/src/molecules/animation/domain/entities/Theme.ts +76 -0
- package/src/molecules/animation/index.ts +146 -0
- package/src/molecules/animation/infrastructure/services/AnimationConfigService.ts +35 -0
- package/src/molecules/animation/infrastructure/services/SpringAnimationConfigService.ts +67 -0
- package/src/molecules/animation/infrastructure/services/TimingAnimationConfigService.ts +57 -0
- package/src/molecules/animation/infrastructure/services/__tests__/SpringAnimationConfigService.test.ts +114 -0
- package/src/molecules/animation/infrastructure/services/__tests__/TimingAnimationConfigService.test.ts +105 -0
- package/src/molecules/animation/presentation/components/Fireworks.tsx +126 -0
- package/src/molecules/animation/presentation/components/__tests__/Fireworks.test.tsx +189 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useAnimation.integration.test.ts +216 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useFireworks.test.ts +242 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useGesture.test.ts +111 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useSpringAnimation.test.ts +131 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useTimingAnimation.test.ts +175 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useTransformAnimation.test.ts +137 -0
- package/src/molecules/animation/presentation/hooks/useAnimation.ts +77 -0
- package/src/molecules/animation/presentation/hooks/useFireworks.ts +141 -0
- package/src/molecules/animation/presentation/hooks/useGesture.ts +61 -0
- package/src/molecules/animation/presentation/hooks/useGestureCreators.ts +163 -0
- package/src/molecules/animation/presentation/hooks/useGestureState.ts +53 -0
- package/src/molecules/animation/presentation/hooks/useIconAnimations.ts +119 -0
- package/src/molecules/animation/presentation/hooks/useModalAnimations.ts +124 -0
- package/src/molecules/animation/presentation/hooks/useReanimatedReady.ts +60 -0
- package/src/molecules/animation/presentation/hooks/useSpringAnimation.ts +69 -0
- package/src/molecules/animation/presentation/hooks/useTimingAnimation.ts +111 -0
- package/src/molecules/animation/presentation/hooks/useTransformAnimation.ts +57 -0
- package/src/molecules/animation/presentation/providers/AnimationThemeProvider.tsx +62 -0
- package/src/molecules/animation/presentation/providers/__tests__/AnimationThemeProvider.test.tsx +165 -0
- package/src/molecules/animation/types/global.d.ts +97 -0
- package/src/molecules/celebration/domain/entities/CelebrationConfig.ts +17 -0
- package/src/molecules/celebration/domain/entities/FireworksConfig.ts +32 -0
- package/src/molecules/celebration/index.ts +93 -0
- package/src/molecules/celebration/infrastructure/services/FireworksConfigService.ts +49 -0
- package/src/molecules/celebration/presentation/components/CelebrationFireworksOverlay.tsx +33 -0
- package/src/molecules/celebration/presentation/components/CelebrationModal.tsx +78 -0
- package/src/molecules/celebration/presentation/components/CelebrationModalContent.tsx +90 -0
- package/src/molecules/celebration/presentation/hooks/useCelebrationModalAnimation.ts +49 -0
- package/src/molecules/celebration/presentation/hooks/useCelebrationState.ts +45 -0
- package/src/molecules/celebration/presentation/styles/CelebrationModalStyles.ts +65 -0
- package/src/molecules/countdown/components/Countdown.tsx +128 -0
- package/src/molecules/countdown/components/CountdownHeader.tsx +84 -0
- package/src/molecules/countdown/components/TimeUnit.tsx +73 -0
- package/src/molecules/countdown/hooks/useCountdown.ts +107 -0
- package/src/molecules/countdown/index.ts +25 -0
- package/src/molecules/countdown/types/CountdownTypes.ts +31 -0
- package/src/molecules/countdown/utils/TimeCalculator.ts +46 -0
- package/src/molecules/emoji/domain/entities/Emoji.ts +129 -0
- package/src/molecules/emoji/index.ts +177 -0
- package/src/molecules/emoji/presentation/components/EmojiPicker.tsx +102 -0
- package/src/molecules/emoji/presentation/hooks/useEmojiPicker.ts +171 -0
- package/src/molecules/index.ts +21 -0
- package/src/molecules/long-press-menu/domain/entities/MenuAction.ts +37 -0
- package/src/molecules/long-press-menu/index.ts +16 -0
- package/src/molecules/navigation/StackNavigator.tsx +75 -0
- package/src/molecules/navigation/TabsNavigator.tsx +94 -0
- package/src/molecules/navigation/components/FabButton.tsx +45 -0
- package/src/molecules/navigation/components/TabLabel.tsx +47 -0
- package/src/molecules/navigation/createStackNavigator.ts +20 -0
- package/src/molecules/navigation/createTabNavigator.ts +20 -0
- package/src/molecules/navigation/hooks/useTabBarStyles.ts +54 -0
- package/src/molecules/navigation/index.ts +37 -0
- package/src/molecules/navigation/types.ts +118 -0
- package/src/molecules/navigation/utils/AppNavigation.ts +101 -0
- package/src/molecules/navigation/utils/IconRenderer.ts +50 -0
- package/src/molecules/navigation/utils/LabelProcessor.ts +70 -0
- package/src/molecules/navigation/utils/NavigationCleanup.ts +62 -0
- package/src/molecules/navigation/utils/NavigationTheme.ts +21 -0
- package/src/molecules/navigation/utils/NavigationValidator.ts +61 -0
- package/src/molecules/navigation/utils/ScreenFactory.ts +115 -0
- package/src/molecules/navigation/utils/__tests__/IconRenderer.getIconName.test.ts +109 -0
- package/src/molecules/navigation/utils/__tests__/IconRenderer.renderIcon.test.ts +116 -0
- package/src/molecules/navigation/utils/__tests__/LabelProcessor.processLabel.test.ts +116 -0
- package/src/molecules/navigation/utils/__tests__/LabelProcessor.processTitle.test.ts +59 -0
- package/src/molecules/navigation/utils/__tests__/NavigationCleanup.test.ts +271 -0
- package/src/molecules/navigation/utils/__tests__/NavigationValidator.test.ts +252 -0
- package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +194 -0
- package/src/molecules/swipe-actions/index.ts +6 -0
- package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +131 -0
- package/src/theme/hooks/useResponsiveDesignTokens.ts +1 -1
- package/src/utilities/clipboard/ClipboardUtils.ts +71 -0
- package/src/utilities/clipboard/index.ts +5 -0
- package/src/utilities/index.ts +6 -0
- package/src/utilities/sharing/domain/entities/Share.ts +210 -0
- package/src/utilities/sharing/index.ts +205 -0
- package/src/utilities/sharing/infrastructure/services/SharingService.ts +165 -0
- 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
|
+
});
|