@umituz/react-native-design-system 2.3.0 → 2.3.2
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 +16 -4
- package/src/atoms/AtomicBadge.tsx +121 -0
- package/src/atoms/AtomicInput.tsx +0 -1
- package/src/atoms/AtomicPicker.tsx +0 -1
- package/src/atoms/index.ts +8 -0
- package/src/atoms/picker/components/PickerChips.tsx +0 -1
- package/src/atoms/picker/components/PickerModal.tsx +1 -3
- package/src/atoms/picker/styles/pickerStyles.ts +1 -1
- package/src/device/domain/entities/Device.ts +207 -0
- package/src/device/domain/entities/DeviceMemoryUtils.ts +62 -0
- package/src/device/domain/entities/DeviceTypeUtils.ts +66 -0
- package/src/device/domain/entities/__tests__/DeviceMemoryUtils.test.ts +118 -0
- package/src/device/domain/entities/__tests__/DeviceTypeUtils.test.ts +104 -0
- package/src/device/domain/entities/__tests__/DeviceUtils.test.ts +167 -0
- package/src/device/index.ts +51 -0
- package/src/device/infrastructure/services/ApplicationInfoService.ts +86 -0
- package/src/device/infrastructure/services/DeviceCapabilityService.ts +60 -0
- package/src/device/infrastructure/services/DeviceIdService.ts +70 -0
- package/src/device/infrastructure/services/DeviceInfoService.ts +95 -0
- package/src/device/infrastructure/services/DeviceService.ts +104 -0
- package/src/device/infrastructure/services/PersistentDeviceIdService.ts +132 -0
- package/src/device/infrastructure/services/UserFriendlyIdService.ts +68 -0
- package/src/device/infrastructure/utils/__tests__/nativeModuleUtils.test.ts +158 -0
- package/src/device/infrastructure/utils/__tests__/stringUtils.test.ts +120 -0
- package/src/device/infrastructure/utils/nativeModuleUtils.ts +69 -0
- package/src/device/infrastructure/utils/stringUtils.ts +59 -0
- package/src/device/presentation/hooks/useAnonymousUser.ts +117 -0
- package/src/device/presentation/hooks/useDeviceInfo.ts +222 -0
- package/src/index.ts +4 -0
- package/src/molecules/ConfirmationModalContent.tsx +4 -4
- package/src/molecules/ConfirmationModalMain.tsx +1 -1
- package/src/molecules/ScreenHeader.tsx +2 -2
- package/src/molecules/confirmation-modal/components.tsx +1 -1
- package/src/molecules/confirmation-modal/styles/confirmationModalStyles.ts +6 -7
- package/src/presentation/utils/variants/__tests__/core.test.ts +0 -1
- package/src/responsive/deviceDetection.ts +5 -5
- package/src/responsive/iPadBreakpoints.ts +55 -0
- package/src/responsive/iPadDetection.ts +48 -0
- package/src/responsive/iPadLayoutUtils.ts +95 -0
- package/src/responsive/iPadModalUtils.ts +98 -0
- package/src/responsive/index.ts +31 -0
- package/src/safe-area/__tests__/components/SafeAreaProvider.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useContentSafeAreaPadding.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useHeaderSafeAreaPadding.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useSafeAreaInsets.test.tsx +2 -2
- package/src/safe-area/__tests__/hooks/useStatusBarSafeAreaPadding.test.tsx +2 -2
- package/src/safe-area/__tests__/integration/completeFlow.test.tsx +5 -4
- package/src/safe-area/__tests__/utils/testUtils.tsx +5 -4
- package/src/theme/infrastructure/stores/themeStore.ts +0 -2
- package/src/typography/presentation/utils/textColorUtils.ts +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Info Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Get device information from native modules
|
|
5
|
+
* Follows SOLID principles - only handles device info retrieval
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Device from 'expo-device';
|
|
9
|
+
import { Platform } from 'react-native';
|
|
10
|
+
import * as Localization from 'expo-localization';
|
|
11
|
+
import type { DeviceInfo } from '../../domain/entities/Device';
|
|
12
|
+
import { safeAccess, withTimeout } from '../utils/nativeModuleUtils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service for retrieving device information
|
|
16
|
+
*/
|
|
17
|
+
export class DeviceInfoService {
|
|
18
|
+
/**
|
|
19
|
+
* Get device information
|
|
20
|
+
* SAFE: Returns minimal info if native modules are not ready
|
|
21
|
+
*/
|
|
22
|
+
static async getDeviceInfo(): Promise<DeviceInfo> {
|
|
23
|
+
try {
|
|
24
|
+
const totalMemory = await withTimeout(
|
|
25
|
+
() => Device.getMaxMemoryAsync(),
|
|
26
|
+
1000,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const brand = safeAccess(() => Device.brand, null);
|
|
30
|
+
const manufacturer = safeAccess(() => Device.manufacturer, null);
|
|
31
|
+
const modelName = safeAccess(() => Device.modelName, null);
|
|
32
|
+
const modelId = safeAccess(() => Device.modelId, null);
|
|
33
|
+
const deviceName = safeAccess(() => Device.deviceName, null);
|
|
34
|
+
const deviceYearClass = safeAccess(() => Device.deviceYearClass, null);
|
|
35
|
+
const deviceType = safeAccess(() => Device.deviceType, null);
|
|
36
|
+
const isDevice = safeAccess(() => Device.isDevice, false);
|
|
37
|
+
const osName = safeAccess(() => Device.osName, null);
|
|
38
|
+
const osVersion = safeAccess(() => Device.osVersion, null);
|
|
39
|
+
const osBuildId = safeAccess(() => Device.osBuildId, null);
|
|
40
|
+
const platformApiLevel = safeAccess(() => Device.platformApiLevel, null);
|
|
41
|
+
|
|
42
|
+
// Localization
|
|
43
|
+
const calendars = Localization.getCalendars();
|
|
44
|
+
const locales = Localization.getLocales();
|
|
45
|
+
const timezone = calendars?.[0]?.timeZone ?? null;
|
|
46
|
+
const region = locales?.[0]?.regionCode ?? null;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
brand,
|
|
50
|
+
manufacturer,
|
|
51
|
+
modelName,
|
|
52
|
+
modelId,
|
|
53
|
+
deviceName,
|
|
54
|
+
deviceYearClass,
|
|
55
|
+
deviceType,
|
|
56
|
+
isDevice,
|
|
57
|
+
osName,
|
|
58
|
+
osVersion,
|
|
59
|
+
osBuildId,
|
|
60
|
+
platformApiLevel,
|
|
61
|
+
totalMemory,
|
|
62
|
+
platform: Platform.OS as 'ios' | 'android' | 'web',
|
|
63
|
+
timezone,
|
|
64
|
+
region,
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return this.getMinimalDeviceInfo();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get minimal device info (fallback)
|
|
73
|
+
*/
|
|
74
|
+
private static getMinimalDeviceInfo(): DeviceInfo {
|
|
75
|
+
return {
|
|
76
|
+
brand: null,
|
|
77
|
+
manufacturer: null,
|
|
78
|
+
modelName: null,
|
|
79
|
+
modelId: null,
|
|
80
|
+
deviceName: null,
|
|
81
|
+
deviceYearClass: null,
|
|
82
|
+
deviceType: null,
|
|
83
|
+
isDevice: false,
|
|
84
|
+
osName: null,
|
|
85
|
+
osVersion: null,
|
|
86
|
+
osBuildId: null,
|
|
87
|
+
platformApiLevel: null,
|
|
88
|
+
totalMemory: null,
|
|
89
|
+
platform: Platform.OS as 'ios' | 'android' | 'web',
|
|
90
|
+
timezone: null,
|
|
91
|
+
region: null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Service - Facade/Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for device operations.
|
|
5
|
+
* Delegates to specialized services following Single Responsibility Principle.
|
|
6
|
+
*
|
|
7
|
+
* @domain device
|
|
8
|
+
* @layer infrastructure/services
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { DeviceInfo, ApplicationInfo, SystemInfo } from '../../domain/entities/Device';
|
|
12
|
+
import { DeviceInfoService } from './DeviceInfoService';
|
|
13
|
+
import { ApplicationInfoService } from './ApplicationInfoService';
|
|
14
|
+
import { DeviceIdService } from './DeviceIdService';
|
|
15
|
+
import { DeviceCapabilityService } from './DeviceCapabilityService';
|
|
16
|
+
import { UserFriendlyIdService } from './UserFriendlyIdService';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Device Service - Facade for all device operations
|
|
20
|
+
*
|
|
21
|
+
* This is a facade that delegates to specialized services.
|
|
22
|
+
* Each service has a single responsibility (SOLID principles).
|
|
23
|
+
*/
|
|
24
|
+
export class DeviceService {
|
|
25
|
+
/**
|
|
26
|
+
* Get device information
|
|
27
|
+
* Delegates to DeviceInfoService
|
|
28
|
+
*/
|
|
29
|
+
static async getDeviceInfo(): Promise<DeviceInfo> {
|
|
30
|
+
return DeviceInfoService.getDeviceInfo();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get application information
|
|
35
|
+
* Delegates to ApplicationInfoService
|
|
36
|
+
*/
|
|
37
|
+
static async getApplicationInfo(): Promise<ApplicationInfo> {
|
|
38
|
+
return ApplicationInfoService.getApplicationInfo();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get complete system information (device + app)
|
|
43
|
+
* @param options - Optional configuration
|
|
44
|
+
* @param options.userId - Optional user ID to include in system info
|
|
45
|
+
*/
|
|
46
|
+
static async getSystemInfo(options?: { userId?: string }): Promise<SystemInfo> {
|
|
47
|
+
const [device, application] = await Promise.all([
|
|
48
|
+
DeviceInfoService.getDeviceInfo(),
|
|
49
|
+
ApplicationInfoService.getApplicationInfo(),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
device,
|
|
54
|
+
application,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
...(options?.userId && { userId: options.userId }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get device unique identifier (platform-specific)
|
|
62
|
+
* Delegates to DeviceIdService
|
|
63
|
+
*/
|
|
64
|
+
static async getDeviceId(): Promise<string | null> {
|
|
65
|
+
return DeviceIdService.getDeviceId();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get offline user ID with fallback
|
|
70
|
+
* Delegates to DeviceIdService
|
|
71
|
+
*/
|
|
72
|
+
static async getOfflineUserId(fallbackId: string = 'offline_user'): Promise<string | null> {
|
|
73
|
+
return DeviceIdService.getOfflineUserId(fallbackId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if device supports specific features
|
|
78
|
+
* Delegates to DeviceCapabilityService
|
|
79
|
+
*/
|
|
80
|
+
static async getDeviceCapabilities(): Promise<{
|
|
81
|
+
isDevice: boolean;
|
|
82
|
+
isTablet: boolean;
|
|
83
|
+
hasNotch: boolean;
|
|
84
|
+
totalMemoryGB: number | null;
|
|
85
|
+
}> {
|
|
86
|
+
return DeviceCapabilityService.getDeviceCapabilities();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if device has notch/dynamic island
|
|
91
|
+
* Delegates to DeviceCapabilityService
|
|
92
|
+
*/
|
|
93
|
+
static async hasNotch(): Promise<boolean> {
|
|
94
|
+
return DeviceCapabilityService.hasNotch();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get user friendly device ID
|
|
99
|
+
* Delegates to UserFriendlyIdService
|
|
100
|
+
*/
|
|
101
|
+
static async getUserFriendlyId(): Promise<string> {
|
|
102
|
+
return UserFriendlyIdService.getUserFriendlyId();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent Device ID Service
|
|
3
|
+
*
|
|
4
|
+
* Provides a stable, persistent device identifier that survives app restarts.
|
|
5
|
+
* Uses native device ID when available, falls back to generated UUID.
|
|
6
|
+
* Stores the ID in AsyncStorage for persistence.
|
|
7
|
+
*
|
|
8
|
+
* @domain device
|
|
9
|
+
* @layer infrastructure/services
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
13
|
+
import { DeviceIdService } from './DeviceIdService';
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = '@device/persistent_id';
|
|
16
|
+
|
|
17
|
+
/** Cached ID to avoid repeated AsyncStorage reads */
|
|
18
|
+
let cachedDeviceId: string | null = null;
|
|
19
|
+
|
|
20
|
+
/** Promise to prevent race conditions during initialization */
|
|
21
|
+
let initializationPromise: Promise<string> | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a UUID v4 without external dependencies
|
|
25
|
+
*/
|
|
26
|
+
function generateUUID(): string {
|
|
27
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
28
|
+
const r = (Math.random() * 16) | 0;
|
|
29
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
30
|
+
return v.toString(16);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Service for managing persistent device identifiers
|
|
36
|
+
*/
|
|
37
|
+
export class PersistentDeviceIdService {
|
|
38
|
+
/**
|
|
39
|
+
* Get or create a persistent device ID
|
|
40
|
+
*
|
|
41
|
+
* This method:
|
|
42
|
+
* 1. Returns cached ID if available (fastest)
|
|
43
|
+
* 2. Checks AsyncStorage for previously stored ID
|
|
44
|
+
* 3. If not found, gets native device ID or generates UUID
|
|
45
|
+
* 4. Stores and caches the result for future calls
|
|
46
|
+
*
|
|
47
|
+
* Thread-safe: Multiple concurrent calls return the same promise
|
|
48
|
+
*/
|
|
49
|
+
static async getDeviceId(): Promise<string> {
|
|
50
|
+
if (cachedDeviceId) {
|
|
51
|
+
return cachedDeviceId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (initializationPromise) {
|
|
55
|
+
return initializationPromise;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
initializationPromise = this.initializeDeviceId();
|
|
59
|
+
return initializationPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize and persist device ID
|
|
64
|
+
*/
|
|
65
|
+
private static async initializeDeviceId(): Promise<string> {
|
|
66
|
+
try {
|
|
67
|
+
const storedId = await AsyncStorage.getItem(STORAGE_KEY);
|
|
68
|
+
|
|
69
|
+
if (storedId) {
|
|
70
|
+
cachedDeviceId = storedId;
|
|
71
|
+
return storedId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const newId = await this.createNewDeviceId();
|
|
75
|
+
await AsyncStorage.setItem(STORAGE_KEY, newId);
|
|
76
|
+
cachedDeviceId = newId;
|
|
77
|
+
|
|
78
|
+
return newId;
|
|
79
|
+
} catch {
|
|
80
|
+
const fallbackId = generateUUID();
|
|
81
|
+
cachedDeviceId = fallbackId;
|
|
82
|
+
return fallbackId;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a new device ID from native source or generate one
|
|
88
|
+
*/
|
|
89
|
+
private static async createNewDeviceId(): Promise<string> {
|
|
90
|
+
const nativeId = await DeviceIdService.getDeviceId();
|
|
91
|
+
|
|
92
|
+
if (nativeId) {
|
|
93
|
+
return `device_${nativeId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return `generated_${generateUUID()}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if device ID exists in storage
|
|
101
|
+
*/
|
|
102
|
+
static async hasStoredId(): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
const storedId = await AsyncStorage.getItem(STORAGE_KEY);
|
|
105
|
+
return storedId !== null;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clear stored device ID (use with caution)
|
|
113
|
+
* This will generate a new ID on next getDeviceId() call
|
|
114
|
+
*/
|
|
115
|
+
static async clearStoredId(): Promise<void> {
|
|
116
|
+
try {
|
|
117
|
+
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
118
|
+
cachedDeviceId = null;
|
|
119
|
+
initializationPromise = null;
|
|
120
|
+
} catch {
|
|
121
|
+
// Silent fail - non-critical operation
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the cached device ID without async operation
|
|
127
|
+
* Returns null if not yet initialized
|
|
128
|
+
*/
|
|
129
|
+
static getCachedId(): string | null {
|
|
130
|
+
return cachedDeviceId;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Friendly ID Service
|
|
3
|
+
*
|
|
4
|
+
* Single Responsibility: Generate user-friendly device identifiers
|
|
5
|
+
* Follows SOLID principles - only handles ID generation logic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
import type { DeviceInfo } from '../../domain/entities/Device';
|
|
10
|
+
import { DeviceInfoService } from './DeviceInfoService';
|
|
11
|
+
import { DeviceIdService } from './DeviceIdService';
|
|
12
|
+
import {
|
|
13
|
+
cleanModelName,
|
|
14
|
+
extractIdPart,
|
|
15
|
+
generateRandomId,
|
|
16
|
+
getPlatformPrefix,
|
|
17
|
+
} from '../utils/stringUtils';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Service for generating user-friendly device IDs
|
|
21
|
+
*/
|
|
22
|
+
export class UserFriendlyIdService {
|
|
23
|
+
/**
|
|
24
|
+
* Get user friendly device ID (e.g. "iPhone13-A8F2")
|
|
25
|
+
*
|
|
26
|
+
* Useful for displaying a readable user identifier in profiles.
|
|
27
|
+
* Combines cleaned model name with short device hash.
|
|
28
|
+
*
|
|
29
|
+
* SAFE: This method has multiple fallback layers to prevent native module crashes.
|
|
30
|
+
* If native modules are not ready, it will return a safe fallback ID.
|
|
31
|
+
*/
|
|
32
|
+
static async getUserFriendlyId(): Promise<string> {
|
|
33
|
+
// Web platform - no native modules needed
|
|
34
|
+
if (Platform.OS === 'web') {
|
|
35
|
+
return `WebUser-${generateRandomId()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Try to get device info and ID
|
|
40
|
+
const deviceInfo: DeviceInfo = await DeviceInfoService.getDeviceInfo();
|
|
41
|
+
const deviceId: string | null = await DeviceIdService.getDeviceId();
|
|
42
|
+
|
|
43
|
+
// If we got device info, use it
|
|
44
|
+
if (deviceInfo && (deviceInfo.modelName || deviceInfo.deviceName)) {
|
|
45
|
+
const model = deviceInfo.modelName || deviceInfo.deviceName || 'Device';
|
|
46
|
+
const cleanModel = cleanModelName(model);
|
|
47
|
+
const idPart = extractIdPart(deviceId, 6);
|
|
48
|
+
|
|
49
|
+
return `${cleanModel}-${idPart}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: Use platform + random ID
|
|
53
|
+
return this.generateFallbackId();
|
|
54
|
+
} catch {
|
|
55
|
+
// Final fallback: Generate safe random ID
|
|
56
|
+
return this.generateFallbackId();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate fallback ID when native modules are not available
|
|
62
|
+
*/
|
|
63
|
+
private static generateFallbackId(): string {
|
|
64
|
+
const platformPrefix = getPlatformPrefix(Platform.OS);
|
|
65
|
+
return `${platformPrefix}-${generateRandomId()}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Module Utils Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { withTimeout, safeAccess, withTimeoutAll } from '../nativeModuleUtils';
|
|
6
|
+
|
|
7
|
+
describe('Native Module Utils', () => {
|
|
8
|
+
describe('withTimeout', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.useFakeTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
jest.useRealTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should resolve with operation result when successful', async () => {
|
|
18
|
+
const operation = jest.fn().mockResolvedValue('success');
|
|
19
|
+
const promise = withTimeout(operation, 1000);
|
|
20
|
+
|
|
21
|
+
jest.advanceTimersByTime(0);
|
|
22
|
+
await expect(promise).resolves.toBe('success');
|
|
23
|
+
expect(operation).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return null when operation times out', async () => {
|
|
27
|
+
const operation = jest.fn().mockImplementation(() => new Promise(() => {}));
|
|
28
|
+
const promise = withTimeout(operation, 1000);
|
|
29
|
+
|
|
30
|
+
jest.advanceTimersByTime(1000);
|
|
31
|
+
await expect(promise).resolves.toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return null when operation throws', async () => {
|
|
35
|
+
const operation = jest.fn().mockRejectedValue(new Error('Test error'));
|
|
36
|
+
const promise = withTimeout(operation, 1000);
|
|
37
|
+
|
|
38
|
+
jest.advanceTimersByTime(0);
|
|
39
|
+
await expect(promise).resolves.toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should use default timeout when not specified', async () => {
|
|
43
|
+
const operation = jest.fn().mockImplementation(() => new Promise(() => {}));
|
|
44
|
+
const promise = withTimeout(operation);
|
|
45
|
+
|
|
46
|
+
jest.advanceTimersByTime(1000);
|
|
47
|
+
await expect(promise).resolves.toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('safeAccess', () => {
|
|
52
|
+
it('should return value when accessor succeeds', () => {
|
|
53
|
+
const accessor = () => 'test value';
|
|
54
|
+
const result = safeAccess(accessor, 'fallback');
|
|
55
|
+
expect(result).toBe('test value');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return fallback when accessor throws', () => {
|
|
59
|
+
const accessor = () => {
|
|
60
|
+
throw new Error('Test error');
|
|
61
|
+
};
|
|
62
|
+
const result = safeAccess(accessor, 'fallback');
|
|
63
|
+
expect(result).toBe('fallback');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return fallback when accessor returns null/undefined', () => {
|
|
67
|
+
const accessor1 = () => null;
|
|
68
|
+
const accessor2 = () => undefined;
|
|
69
|
+
|
|
70
|
+
expect(safeAccess(accessor1, 'fallback')).toBe('fallback');
|
|
71
|
+
expect(safeAccess(accessor2, 'fallback')).toBe('fallback');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return value when accessor returns falsy but not null/undefined', () => {
|
|
75
|
+
const accessor1 = () => '';
|
|
76
|
+
const accessor2 = () => 0;
|
|
77
|
+
const accessor3 = () => false;
|
|
78
|
+
|
|
79
|
+
expect(safeAccess(accessor1, 'fallback')).toBe('');
|
|
80
|
+
expect(safeAccess(accessor2, 'fallback')).toBe(0);
|
|
81
|
+
expect(safeAccess(accessor3, 'fallback')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('withTimeoutAll', () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
jest.useFakeTimers();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
jest.useRealTimers();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should resolve all operations when successful', async () => {
|
|
95
|
+
const operations = [
|
|
96
|
+
jest.fn().mockResolvedValue('result1'),
|
|
97
|
+
jest.fn().mockResolvedValue('result2'),
|
|
98
|
+
jest.fn().mockResolvedValue('result3'),
|
|
99
|
+
];
|
|
100
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
101
|
+
|
|
102
|
+
jest.advanceTimersByTime(0);
|
|
103
|
+
const results = await promise;
|
|
104
|
+
|
|
105
|
+
expect(results).toEqual(['result1', 'result2', 'result3']);
|
|
106
|
+
operations.forEach(op => expect(op).toHaveBeenCalledTimes(1));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle individual operation failures', async () => {
|
|
110
|
+
const operations = [
|
|
111
|
+
jest.fn().mockResolvedValue('result1'),
|
|
112
|
+
jest.fn().mockRejectedValue(new Error('Test error')),
|
|
113
|
+
jest.fn().mockResolvedValue('result3'),
|
|
114
|
+
];
|
|
115
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
116
|
+
|
|
117
|
+
jest.advanceTimersByTime(0);
|
|
118
|
+
const results = await promise;
|
|
119
|
+
|
|
120
|
+
expect(results).toEqual(['result1', null, 'result3']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return null for all operations when timeout occurs', async () => {
|
|
124
|
+
const operations = [
|
|
125
|
+
jest.fn().mockImplementation(() => new Promise(() => {})),
|
|
126
|
+
jest.fn().mockResolvedValue('result2'),
|
|
127
|
+
];
|
|
128
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
129
|
+
|
|
130
|
+
jest.advanceTimersByTime(1000);
|
|
131
|
+
const results = await promise;
|
|
132
|
+
|
|
133
|
+
expect(results).toEqual([null, null]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should use default timeout when not specified', async () => {
|
|
137
|
+
const operations = [
|
|
138
|
+
jest.fn().mockImplementation(() => new Promise(() => {})),
|
|
139
|
+
];
|
|
140
|
+
const promise = withTimeoutAll(operations);
|
|
141
|
+
|
|
142
|
+
jest.advanceTimersByTime(2000);
|
|
143
|
+
const results = await promise;
|
|
144
|
+
|
|
145
|
+
expect(results).toEqual([null]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle empty operations array', async () => {
|
|
149
|
+
const operations: Array<() => Promise<string>> = [];
|
|
150
|
+
const promise = withTimeoutAll(operations, 1000);
|
|
151
|
+
|
|
152
|
+
jest.advanceTimersByTime(0);
|
|
153
|
+
const results = await promise;
|
|
154
|
+
|
|
155
|
+
expect(results).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String Utils Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cleanModelName, extractIdPart, generateRandomId, getPlatformPrefix } from '../stringUtils';
|
|
6
|
+
|
|
7
|
+
describe('String Utils', () => {
|
|
8
|
+
describe('cleanModelName', () => {
|
|
9
|
+
it('should remove special characters from model name', () => {
|
|
10
|
+
const result = cleanModelName('iPhone 14 Pro Max');
|
|
11
|
+
expect(result).toBe('iPhone14ProMax');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should handle null/undefined input', () => {
|
|
15
|
+
expect(cleanModelName(null)).toBe('Device');
|
|
16
|
+
expect(cleanModelName(undefined)).toBe('Device');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle empty string', () => {
|
|
20
|
+
const result = cleanModelName('');
|
|
21
|
+
expect(result).toBe('Device');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle model with only special characters', () => {
|
|
25
|
+
const result = cleanModelName('!@#$%^&*()');
|
|
26
|
+
expect(result).toBe('Device');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should preserve alphanumeric characters', () => {
|
|
30
|
+
const result = cleanModelName('SM-G998B');
|
|
31
|
+
expect(result).toBe('SMG998B');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('extractIdPart', () => {
|
|
36
|
+
it('should extract last N characters from device ID', () => {
|
|
37
|
+
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
38
|
+
const result = extractIdPart(deviceId, 6);
|
|
39
|
+
expect(result).toBe('789012');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should use default length of 6 when not specified', () => {
|
|
43
|
+
const deviceId = '12345678-1234-1234-1234-123456789012';
|
|
44
|
+
const result = extractIdPart(deviceId);
|
|
45
|
+
expect(result).toBe('789012');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle null device ID by generating random ID', () => {
|
|
49
|
+
const result = extractIdPart(null, 6);
|
|
50
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle empty device ID by generating random ID', () => {
|
|
54
|
+
const result = extractIdPart('', 6);
|
|
55
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle device ID shorter than requested length', () => {
|
|
59
|
+
const deviceId = 'ABC';
|
|
60
|
+
const result = extractIdPart(deviceId, 6);
|
|
61
|
+
expect(result).toBe('ABC');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should convert to uppercase', () => {
|
|
65
|
+
const deviceId = 'abcdef';
|
|
66
|
+
const result = extractIdPart(deviceId, 3);
|
|
67
|
+
expect(result).toBe('DEF');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('generateRandomId', () => {
|
|
72
|
+
it('should generate random ID with default length', () => {
|
|
73
|
+
const result = generateRandomId();
|
|
74
|
+
expect(result).toMatch(/^[A-Z0-9]{6}$/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should generate random ID with specified length', () => {
|
|
78
|
+
const result = generateRandomId(10);
|
|
79
|
+
expect(result).toMatch(/^[A-Z0-9]{10}$/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should generate different IDs on multiple calls', () => {
|
|
83
|
+
const id1 = generateRandomId(6);
|
|
84
|
+
const id2 = generateRandomId(6);
|
|
85
|
+
expect(id1).not.toBe(id2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle length of 1', () => {
|
|
89
|
+
const result = generateRandomId(1);
|
|
90
|
+
expect(result).toMatch(/^[A-Z0-9]{1}$/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getPlatformPrefix', () => {
|
|
95
|
+
it('should return iOS for ios platform', () => {
|
|
96
|
+
const result = getPlatformPrefix('ios');
|
|
97
|
+
expect(result).toBe('iOS');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should return Android for android platform', () => {
|
|
101
|
+
const result = getPlatformPrefix('android');
|
|
102
|
+
expect(result).toBe('Android');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should return Device for unknown platform', () => {
|
|
106
|
+
const result = getPlatformPrefix('windows');
|
|
107
|
+
expect(result).toBe('Device');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return Device for empty string', () => {
|
|
111
|
+
const result = getPlatformPrefix('');
|
|
112
|
+
expect(result).toBe('Device');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should be case sensitive', () => {
|
|
116
|
+
expect(getPlatformPrefix('IOS')).toBe('Device');
|
|
117
|
+
expect(getPlatformPrefix('ANDROID')).toBe('Device');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|