From deddd3d5bc875db3687639ae63e23f31878134f0 Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 29 Jan 2026 12:05:29 -0800 Subject: [PATCH] Add comprehensive null safety to navigation system Implemented null/undefined handling throughout NavigationController and useNavigationFlow hook to prevent crashes from invalid data: - Added null checks for all profile and beneficiary parameters - Validated beneficiary IDs before navigation (type and value checks) - Added fallback routes when data is invalid or missing - Implemented safe navigation with error handling and logging - Added defensive guards for optional purchaseResult parameter Key improvements: - getRouteAfterLogin: handles null profile, null beneficiaries, invalid IDs - getRouteForBeneficiarySetup: validates beneficiary exists before routing - getRouteAfterAddBeneficiary: validates beneficiary ID type and value - getRouteAfterPurchase: handles null purchaseResult safely - getBeneficiaryRoute: returns fallback route for invalid beneficiaries - navigate hook: wraps router calls in try-catch with validation All methods now gracefully handle edge cases without crashing, logging warnings for debugging while maintaining UX flow. Tests included for all null/undefined scenarios. --- .../useNavigationFlow.nullsafety.test.tsx | 424 ++++++++++++++++++ hooks/useNavigationFlow.ts | 52 ++- services/NavigationController.ts | 79 +++- .../NavigationController.nullsafety.test.ts | 424 ++++++++++++++++++ 4 files changed, 951 insertions(+), 28 deletions(-) create mode 100644 hooks/__tests__/useNavigationFlow.nullsafety.test.tsx create mode 100644 services/__tests__/NavigationController.nullsafety.test.ts diff --git a/hooks/__tests__/useNavigationFlow.nullsafety.test.tsx b/hooks/__tests__/useNavigationFlow.nullsafety.test.tsx new file mode 100644 index 0000000..1e8cacb --- /dev/null +++ b/hooks/__tests__/useNavigationFlow.nullsafety.test.tsx @@ -0,0 +1,424 @@ +/** + * useNavigationFlow Hook - Null Safety Tests + * + * Tests for null/undefined handling in navigation hook + */ + +import { renderHook, act } from '@testing-library/react-native'; +import { useNavigationFlow } from '../useNavigationFlow'; +import { ROUTES } from '@/services/NavigationController'; +import type { Beneficiary } from '@/types'; + +// Mock expo-router +const mockPush = jest.fn(); +const mockReplace = jest.fn(); + +jest.mock('expo-router', () => ({ + useRouter: () => ({ + push: mockPush, + replace: mockReplace, + }), +})); + +describe('useNavigationFlow - Null Safety', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console warnings during tests + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('navigate', () => { + it('should handle null navigation result', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigate(null as any); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid navigation result:', + null + ); + }); + + it('should handle undefined navigation result', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigate(undefined as any); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid navigation result:', + undefined + ); + }); + + it('should handle navigation result without path', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigate({ path: '' } as any); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalled(); + }); + + it('should navigate successfully with valid result', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigate({ path: ROUTES.TABS.DASHBOARD }); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.TABS.DASHBOARD); + }); + + it('should use replace when specified', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigate({ path: ROUTES.TABS.DASHBOARD }, true); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.TABS.DASHBOARD); + expect(mockPush).not.toHaveBeenCalled(); + }); + }); + + describe('navigateAfterLogin', () => { + it('should handle null profile', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterLogin(null, []); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.AUTH.ENTER_NAME); + }); + + it('should handle undefined profile', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterLogin(undefined, []); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.AUTH.ENTER_NAME); + }); + + it('should handle null beneficiaries', () => { + const { result } = renderHook(() => useNavigationFlow()); + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + + act(() => { + result.current.navigateAfterLogin(profile, null); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiaries', () => { + const { result } = renderHook(() => useNavigationFlow()); + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + + act(() => { + result.current.navigateAfterLogin(profile, undefined); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + }); + + describe('navigateAfterAddBeneficiary', () => { + it('should handle null beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterAddBeneficiary(null, false); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterAddBeneficiary(undefined, false); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should navigate successfully with valid beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterAddBeneficiary(1, false); + }); + + expect(mockPush).toHaveBeenCalledWith({ + pathname: ROUTES.AUTH.PURCHASE, + params: { beneficiaryId: 1 }, + }); + }); + }); + + describe('navigateAfterPurchase', () => { + it('should handle null beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterPurchase(null, {}); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterPurchase(undefined, {}); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle null options', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterPurchase(1, null); + }); + + expect(mockPush).toHaveBeenCalled(); + }); + + it('should handle undefined options', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateAfterPurchase(1, undefined); + }); + + expect(mockPush).toHaveBeenCalled(); + }); + }); + + describe('navigateToBeneficiary', () => { + it('should handle null beneficiary', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateToBeneficiary(null, 'view'); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.TABS.BENEFICIARIES); + }); + + it('should handle undefined beneficiary', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.navigateToBeneficiary(undefined, 'view'); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.TABS.BENEFICIARIES); + }); + + it('should navigate successfully with valid beneficiary', () => { + const { result } = renderHook(() => useNavigationFlow()); + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + }; + + act(() => { + result.current.navigateToBeneficiary(beneficiary, 'view'); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.BENEFICIARY.DETAIL(1)); + }); + }); + + describe('goToPurchase', () => { + it('should handle null beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToPurchase(null); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid beneficiaryId for purchase:', + null + ); + }); + + it('should handle undefined beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToPurchase(undefined); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid beneficiaryId for purchase:', + undefined + ); + }); + + it('should handle invalid beneficiaryId type', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToPurchase('invalid' as any); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid beneficiaryId for purchase:', + 'invalid' + ); + }); + + it('should navigate successfully with valid beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToPurchase(1); + }); + + expect(mockPush).toHaveBeenCalledWith({ + pathname: ROUTES.AUTH.PURCHASE, + params: { beneficiaryId: 1 }, + }); + }); + }); + + describe('goToActivate', () => { + it('should handle null beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToActivate(null); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid beneficiaryId for activation:', + null + ); + }); + + it('should handle undefined beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToActivate(undefined); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalledWith( + '[useNavigationFlow] Invalid beneficiaryId for activation:', + undefined + ); + }); + + it('should navigate successfully with valid beneficiaryId', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToActivate(1); + }); + + expect(mockPush).toHaveBeenCalledWith({ + pathname: ROUTES.AUTH.ACTIVATE, + params: { beneficiaryId: 1 }, + }); + }); + + it('should include demo param when provided', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToActivate(1, true); + }); + + expect(mockPush).toHaveBeenCalledWith({ + pathname: ROUTES.AUTH.ACTIVATE, + params: { beneficiaryId: 1, demo: true }, + }); + }); + }); + + describe('navigation shortcuts', () => { + it('should navigate to dashboard', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToDashboard(); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.TABS.DASHBOARD); + }); + + it('should navigate to login', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToLogin(); + }); + + expect(mockReplace).toHaveBeenCalledWith(ROUTES.AUTH.LOGIN); + }); + + it('should navigate to add beneficiary', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToAddBeneficiary(); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should navigate to profile', () => { + const { result } = renderHook(() => useNavigationFlow()); + + act(() => { + result.current.goToProfile(); + }); + + expect(mockPush).toHaveBeenCalledWith(ROUTES.TABS.PROFILE); + }); + }); +}); diff --git a/hooks/useNavigationFlow.ts b/hooks/useNavigationFlow.ts index 5fe72bb..50dae5e 100644 --- a/hooks/useNavigationFlow.ts +++ b/hooks/useNavigationFlow.ts @@ -21,15 +21,25 @@ export function useNavigationFlow() { /** * Execute navigation based on NavigationResult */ - const navigate = useCallback((result: NavigationResult, replace = false) => { + const navigate = useCallback((result: NavigationResult | null | undefined, replace = false) => { + // Null safety: validate navigation result + if (!result || !result.path) { + console.warn('[useNavigationFlow] Invalid navigation result:', result); + return; + } + const href = result.params ? { pathname: result.path, params: result.params } : result.path; - if (replace) { - router.replace(href as any); - } else { - router.push(href as any); + try { + if (replace) { + router.replace(href as any); + } else { + router.push(href as any); + } + } catch (error) { + console.error('[useNavigationFlow] Navigation error:', error); } }, [router]); @@ -37,8 +47,8 @@ export function useNavigationFlow() { * Navigate after successful login/OTP verification */ const navigateAfterLogin = useCallback(( - profile: UserProfile | null, - beneficiaries: Beneficiary[] + profile: UserProfile | null | undefined, + beneficiaries: Beneficiary[] | null | undefined ) => { const result = NavigationController.getRouteAfterLogin(profile, beneficiaries); navigate(result, true); // replace to prevent going back to login @@ -48,7 +58,7 @@ export function useNavigationFlow() { * Navigate after creating a new beneficiary */ const navigateAfterAddBeneficiary = useCallback(( - beneficiaryId: number, + beneficiaryId: number | null | undefined, hasExistingDevices: boolean ) => { const result = NavigationController.getRouteAfterAddBeneficiary( @@ -62,8 +72,8 @@ export function useNavigationFlow() { * Navigate after purchase completion */ const navigateAfterPurchase = useCallback(( - beneficiaryId: number, - options: { skipToActivate?: boolean; demo?: boolean } = {} + beneficiaryId: number | null | undefined, + options: { skipToActivate?: boolean; demo?: boolean } | null | undefined = {} ) => { const result = NavigationController.getRouteAfterPurchase(beneficiaryId, options); navigate(result); @@ -72,7 +82,7 @@ export function useNavigationFlow() { /** * Navigate after device activation */ - const navigateAfterActivation = useCallback((beneficiaryId: number) => { + const navigateAfterActivation = useCallback((beneficiaryId: number | null | undefined) => { const result = NavigationController.getRouteAfterActivation(beneficiaryId); navigate(result, true); // replace to prevent going back }, [navigate]); @@ -81,7 +91,7 @@ export function useNavigationFlow() { * Navigate to beneficiary-specific screens */ const navigateToBeneficiary = useCallback(( - beneficiary: Beneficiary, + beneficiary: Beneficiary | null | undefined, action: 'view' | 'subscription' | 'equipment' | 'share' = 'view' ) => { const result = NavigationController.getBeneficiaryRoute(beneficiary, action); @@ -91,7 +101,7 @@ export function useNavigationFlow() { /** * Navigate to beneficiary setup flow (purchase or activate) */ - const navigateToBeneficiarySetup = useCallback((beneficiary: Beneficiary) => { + const navigateToBeneficiarySetup = useCallback((beneficiary: Beneficiary | null | undefined) => { const result = NavigationController.getRouteForBeneficiarySetup(beneficiary); navigate(result); }, [navigate]); @@ -122,14 +132,26 @@ export function useNavigationFlow() { navigate({ path: ROUTES.AUTH.ADD_LOVED_ONE }); }, [navigate]); - const goToPurchase = useCallback((beneficiaryId: number) => { + const goToPurchase = useCallback((beneficiaryId: number | null | undefined) => { + // Null safety: only navigate if beneficiaryId is valid + if (!beneficiaryId || typeof beneficiaryId !== 'number') { + console.warn('[useNavigationFlow] Invalid beneficiaryId for purchase:', beneficiaryId); + return; + } + navigate({ path: ROUTES.AUTH.PURCHASE, params: { beneficiaryId }, }); }, [navigate]); - const goToActivate = useCallback((beneficiaryId: number, demo?: boolean) => { + const goToActivate = useCallback((beneficiaryId: number | null | undefined, demo?: boolean) => { + // Null safety: only navigate if beneficiaryId is valid + if (!beneficiaryId || typeof beneficiaryId !== 'number') { + console.warn('[useNavigationFlow] Invalid beneficiaryId for activation:', beneficiaryId); + return; + } + navigate({ path: ROUTES.AUTH.ACTIVATE, params: demo !== undefined ? { beneficiaryId, demo } : { beneficiaryId }, diff --git a/services/NavigationController.ts b/services/NavigationController.ts index 2c89565..36a2b14 100644 --- a/services/NavigationController.ts +++ b/services/NavigationController.ts @@ -86,8 +86,8 @@ export const NavigationController = { * 4. User has active beneficiary → dashboard */ getRouteAfterLogin( - profile: UserProfile | null, - beneficiaries: Beneficiary[] + profile: UserProfile | null | undefined, + beneficiaries: Beneficiary[] | null | undefined ): NavigationResult { // Step 1: Check if user has name if (!profile?.firstName) { @@ -112,6 +112,14 @@ export const NavigationController = { // Step 4: Single beneficiary - check if needs setup const singleBeneficiary = beneficiaries[0]; + + // Null safety: verify beneficiary exists and has valid id + if (!singleBeneficiary || typeof singleBeneficiary.id !== 'number') { + return { + path: ROUTES.AUTH.ADD_LOVED_ONE, + }; + } + const isActive = singleBeneficiary.hasDevices || singleBeneficiary.equipmentStatus === 'active' || singleBeneficiary.equipmentStatus === 'demo'; @@ -130,7 +138,14 @@ export const NavigationController = { /** * Determine where to navigate for beneficiary equipment setup */ - getRouteForBeneficiarySetup(beneficiary: Beneficiary): NavigationResult { + getRouteForBeneficiarySetup(beneficiary: Beneficiary | null | undefined): NavigationResult { + // Null safety: validate beneficiary exists and has valid id + if (!beneficiary || typeof beneficiary.id !== 'number') { + return { + path: ROUTES.AUTH.ADD_LOVED_ONE, + }; + } + const status = beneficiary.equipmentStatus || 'none'; switch (status) { @@ -179,9 +194,16 @@ export const NavigationController = { * @param hasExistingDevices - User indicated they already have WellNuo devices */ getRouteAfterAddBeneficiary( - beneficiaryId: number, + beneficiaryId: number | null | undefined, hasExistingDevices: boolean ): NavigationResult { + // Null safety: validate beneficiary ID + if (!beneficiaryId || typeof beneficiaryId !== 'number') { + return { + path: ROUTES.AUTH.ADD_LOVED_ONE, + }; + } + if (hasExistingDevices) { // User has existing devices - go directly to activation return { @@ -201,10 +223,17 @@ export const NavigationController = { * After successful purchase, navigate to next step */ getRouteAfterPurchase( - beneficiaryId: number, - purchaseResult: { skipToActivate?: boolean; demo?: boolean } + beneficiaryId: number | null | undefined, + purchaseResult: { skipToActivate?: boolean; demo?: boolean } | null | undefined ): NavigationResult { - if (purchaseResult.demo || purchaseResult.skipToActivate) { + // Null safety: validate beneficiary ID + if (!beneficiaryId || typeof beneficiaryId !== 'number') { + return { + path: ROUTES.AUTH.ADD_LOVED_ONE, + }; + } + + if (purchaseResult?.demo || purchaseResult?.skipToActivate) { // Demo mode or skip - go to activate return { path: ROUTES.AUTH.ACTIVATE, @@ -225,7 +254,14 @@ export const NavigationController = { /** * After successful activation, navigate to beneficiary detail page */ - getRouteAfterActivation(beneficiaryId: number): NavigationResult { + getRouteAfterActivation(beneficiaryId: number | null | undefined): NavigationResult { + // Null safety: validate beneficiary ID, fallback to dashboard + if (!beneficiaryId || typeof beneficiaryId !== 'number') { + return { + path: ROUTES.TABS.DASHBOARD, + }; + } + return { path: ROUTES.BENEFICIARY.DETAIL(beneficiaryId), params: { justActivated: true }, @@ -238,8 +274,8 @@ export const NavigationController = { * Used when user logs in with existing account */ getRouteForReturningUser( - profile: UserProfile, - beneficiaries: Beneficiary[] + profile: UserProfile | null | undefined, + beneficiaries: Beneficiary[] | null | undefined ): NavigationResult { // Same logic as after login, but could add welcome-back screen return this.getRouteAfterLogin(profile, beneficiaries); @@ -259,9 +295,16 @@ export const NavigationController = { * Get route for a specific beneficiary action */ getBeneficiaryRoute( - beneficiary: Beneficiary, + beneficiary: Beneficiary | null | undefined, action: 'view' | 'subscription' | 'equipment' | 'share' ): NavigationResult { + // Null safety: validate beneficiary exists and has valid id + if (!beneficiary || typeof beneficiary.id !== 'number') { + return { + path: ROUTES.TABS.BENEFICIARIES, + }; + } + switch (action) { case 'view': return { path: ROUTES.BENEFICIARY.DETAIL(beneficiary.id) }; @@ -279,7 +322,12 @@ export const NavigationController = { /** * Determine if beneficiary card should show "Setup Equipment" button */ - shouldShowEquipmentSetup(beneficiary: Beneficiary): boolean { + shouldShowEquipmentSetup(beneficiary: Beneficiary | null | undefined): boolean { + // Null safety: return false if beneficiary is invalid + if (!beneficiary) { + return false; + } + return !beneficiary.hasDevices && beneficiary.equipmentStatus !== 'active' && beneficiary.equipmentStatus !== 'demo'; @@ -289,8 +337,13 @@ export const NavigationController = { * Get call-to-action for beneficiary based on status */ getBeneficiaryCallToAction( - beneficiary: Beneficiary + beneficiary: Beneficiary | null | undefined ): { label: string; action: () => NavigationResult } | null { + // Null safety: return null if beneficiary is invalid + if (!beneficiary || typeof beneficiary.id !== 'number') { + return null; + } + const status = beneficiary.equipmentStatus || 'none'; if (beneficiary.hasDevices || status === 'active' || status === 'demo') { diff --git a/services/__tests__/NavigationController.nullsafety.test.ts b/services/__tests__/NavigationController.nullsafety.test.ts new file mode 100644 index 0000000..7048187 --- /dev/null +++ b/services/__tests__/NavigationController.nullsafety.test.ts @@ -0,0 +1,424 @@ +/** + * NavigationController Null Safety Tests + * + * Tests for null/undefined handling and edge cases in navigation logic + */ + +import { NavigationController, ROUTES } from '../NavigationController'; +import type { Beneficiary, EquipmentStatus } from '@/types'; + +describe('NavigationController - Null Safety', () => { + describe('getRouteAfterLogin', () => { + it('should handle null profile', () => { + const result = NavigationController.getRouteAfterLogin(null, []); + expect(result.path).toBe(ROUTES.AUTH.ENTER_NAME); + }); + + it('should handle undefined profile', () => { + const result = NavigationController.getRouteAfterLogin(undefined, []); + expect(result.path).toBe(ROUTES.AUTH.ENTER_NAME); + }); + + it('should handle profile without firstName', () => { + const profile = { + id: 1, + email: 'test@example.com', + firstName: null, + lastName: null, + phone: null, + }; + const result = NavigationController.getRouteAfterLogin(profile, []); + expect(result.path).toBe(ROUTES.AUTH.ENTER_NAME); + }); + + it('should handle null beneficiaries array', () => { + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + const result = NavigationController.getRouteAfterLogin(profile, null); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiaries array', () => { + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + const result = NavigationController.getRouteAfterLogin(profile, undefined); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle empty beneficiaries array', () => { + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + const result = NavigationController.getRouteAfterLogin(profile, []); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle beneficiary with invalid id', () => { + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + const beneficiaries = [ + { + id: null as any, + name: 'Mom', + displayName: 'Mom', + status: 'online' as const, + }, + ]; + const result = NavigationController.getRouteAfterLogin(profile, beneficiaries); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should navigate to dashboard for active beneficiary', () => { + const profile = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: null, + }; + const beneficiaries: Beneficiary[] = [ + { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + hasDevices: true, + equipmentStatus: 'active', + }, + ]; + const result = NavigationController.getRouteAfterLogin(profile, beneficiaries); + expect(result.path).toBe(ROUTES.TABS.DASHBOARD); + }); + }); + + describe('getRouteForBeneficiarySetup', () => { + it('should handle null beneficiary', () => { + const result = NavigationController.getRouteForBeneficiarySetup(null); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiary', () => { + const result = NavigationController.getRouteForBeneficiarySetup(undefined); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle beneficiary with invalid id', () => { + const beneficiary = { + id: null as any, + name: 'Mom', + displayName: 'Mom', + status: 'online' as const, + }; + const result = NavigationController.getRouteForBeneficiarySetup(beneficiary); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should navigate to purchase for beneficiary with no equipment', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + equipmentStatus: 'none', + }; + const result = NavigationController.getRouteForBeneficiarySetup(beneficiary); + expect(result.path).toBe(ROUTES.AUTH.PURCHASE); + expect(result.params).toEqual({ beneficiaryId: 1 }); + }); + + it('should navigate to equipment page for ordered status', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + equipmentStatus: 'ordered', + }; + const result = NavigationController.getRouteForBeneficiarySetup(beneficiary); + expect(result.path).toBe(ROUTES.BENEFICIARY.EQUIPMENT(1)); + }); + }); + + describe('getRouteAfterAddBeneficiary', () => { + it('should handle null beneficiaryId', () => { + const result = NavigationController.getRouteAfterAddBeneficiary(null, false); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiaryId', () => { + const result = NavigationController.getRouteAfterAddBeneficiary(undefined, false); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle invalid beneficiaryId type', () => { + const result = NavigationController.getRouteAfterAddBeneficiary('invalid' as any, false); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should navigate to purchase for new equipment', () => { + const result = NavigationController.getRouteAfterAddBeneficiary(1, false); + expect(result.path).toBe(ROUTES.AUTH.PURCHASE); + expect(result.params).toEqual({ beneficiaryId: 1 }); + }); + + it('should navigate to activate for existing devices', () => { + const result = NavigationController.getRouteAfterAddBeneficiary(1, true); + expect(result.path).toBe(ROUTES.AUTH.ACTIVATE); + expect(result.params).toEqual({ beneficiaryId: 1 }); + }); + }); + + describe('getRouteAfterPurchase', () => { + it('should handle null beneficiaryId', () => { + const result = NavigationController.getRouteAfterPurchase(null, {}); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle undefined beneficiaryId', () => { + const result = NavigationController.getRouteAfterPurchase(undefined, {}); + expect(result.path).toBe(ROUTES.AUTH.ADD_LOVED_ONE); + }); + + it('should handle null purchaseResult', () => { + const result = NavigationController.getRouteAfterPurchase(1, null); + expect(result.path).toBe(ROUTES.BENEFICIARY.EQUIPMENT(1)); + }); + + it('should handle undefined purchaseResult', () => { + const result = NavigationController.getRouteAfterPurchase(1, undefined); + expect(result.path).toBe(ROUTES.BENEFICIARY.EQUIPMENT(1)); + }); + + it('should navigate to activate for demo mode', () => { + const result = NavigationController.getRouteAfterPurchase(1, { demo: true }); + expect(result.path).toBe(ROUTES.AUTH.ACTIVATE); + expect(result.params).toEqual({ beneficiaryId: 1, demo: true }); + }); + + it('should navigate to equipment page for normal purchase', () => { + const result = NavigationController.getRouteAfterPurchase(1, {}); + expect(result.path).toBe(ROUTES.BENEFICIARY.EQUIPMENT(1)); + }); + }); + + describe('getRouteAfterActivation', () => { + it('should handle null beneficiaryId', () => { + const result = NavigationController.getRouteAfterActivation(null); + expect(result.path).toBe(ROUTES.TABS.DASHBOARD); + }); + + it('should handle undefined beneficiaryId', () => { + const result = NavigationController.getRouteAfterActivation(undefined); + expect(result.path).toBe(ROUTES.TABS.DASHBOARD); + }); + + it('should handle invalid beneficiaryId type', () => { + const result = NavigationController.getRouteAfterActivation('invalid' as any); + expect(result.path).toBe(ROUTES.TABS.DASHBOARD); + }); + + it('should navigate to beneficiary detail for valid id', () => { + const result = NavigationController.getRouteAfterActivation(1); + expect(result.path).toBe(ROUTES.BENEFICIARY.DETAIL(1)); + expect(result.params).toEqual({ justActivated: true }); + }); + }); + + describe('getBeneficiaryRoute', () => { + it('should handle null beneficiary', () => { + const result = NavigationController.getBeneficiaryRoute(null, 'view'); + expect(result.path).toBe(ROUTES.TABS.BENEFICIARIES); + }); + + it('should handle undefined beneficiary', () => { + const result = NavigationController.getBeneficiaryRoute(undefined, 'view'); + expect(result.path).toBe(ROUTES.TABS.BENEFICIARIES); + }); + + it('should handle beneficiary with invalid id', () => { + const beneficiary = { + id: null as any, + name: 'Mom', + displayName: 'Mom', + status: 'online' as const, + }; + const result = NavigationController.getBeneficiaryRoute(beneficiary, 'view'); + expect(result.path).toBe(ROUTES.TABS.BENEFICIARIES); + }); + + it('should navigate to view for valid beneficiary', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + }; + const result = NavigationController.getBeneficiaryRoute(beneficiary, 'view'); + expect(result.path).toBe(ROUTES.BENEFICIARY.DETAIL(1)); + }); + + it('should navigate to subscription for valid beneficiary', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + }; + const result = NavigationController.getBeneficiaryRoute(beneficiary, 'subscription'); + expect(result.path).toBe(ROUTES.BENEFICIARY.SUBSCRIPTION(1)); + }); + }); + + describe('shouldShowEquipmentSetup', () => { + it('should return false for null beneficiary', () => { + const result = NavigationController.shouldShowEquipmentSetup(null); + expect(result).toBe(false); + }); + + it('should return false for undefined beneficiary', () => { + const result = NavigationController.shouldShowEquipmentSetup(undefined); + expect(result).toBe(false); + }); + + it('should return true for beneficiary without devices', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + hasDevices: false, + equipmentStatus: 'none', + }; + const result = NavigationController.shouldShowEquipmentSetup(beneficiary); + expect(result).toBe(true); + }); + + it('should return false for beneficiary with devices', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + hasDevices: true, + equipmentStatus: 'active', + }; + const result = NavigationController.shouldShowEquipmentSetup(beneficiary); + expect(result).toBe(false); + }); + }); + + describe('getBeneficiaryCallToAction', () => { + it('should return null for null beneficiary', () => { + const result = NavigationController.getBeneficiaryCallToAction(null); + expect(result).toBe(null); + }); + + it('should return null for undefined beneficiary', () => { + const result = NavigationController.getBeneficiaryCallToAction(undefined); + expect(result).toBe(null); + }); + + it('should return null for beneficiary with invalid id', () => { + const beneficiary = { + id: null as any, + name: 'Mom', + displayName: 'Mom', + status: 'online' as const, + }; + const result = NavigationController.getBeneficiaryCallToAction(beneficiary); + expect(result).toBe(null); + }); + + it('should return null for active beneficiary', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + hasDevices: true, + equipmentStatus: 'active', + }; + const result = NavigationController.getBeneficiaryCallToAction(beneficiary); + expect(result).toBe(null); + }); + + it('should return setup action for beneficiary with no equipment', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + hasDevices: false, + equipmentStatus: 'none', + }; + const result = NavigationController.getBeneficiaryCallToAction(beneficiary); + expect(result).not.toBe(null); + expect(result?.label).toBe('Setup Equipment'); + }); + + it('should return track delivery action for ordered equipment', () => { + const beneficiary: Beneficiary = { + id: 1, + name: 'Mom', + displayName: 'Mom', + status: 'online', + hasDevices: false, + equipmentStatus: 'ordered', + }; + const result = NavigationController.getBeneficiaryCallToAction(beneficiary); + expect(result).not.toBe(null); + expect(result?.label).toBe('Track Delivery'); + }); + }); + + describe('getEquipmentStatusText', () => { + it('should handle undefined status', () => { + const result = NavigationController.getEquipmentStatusText(undefined); + expect(result).toBe('Unknown'); + }); + + it('should return correct text for each status', () => { + expect(NavigationController.getEquipmentStatusText('none')).toBe('No equipment'); + expect(NavigationController.getEquipmentStatusText('ordered')).toBe('Equipment ordered'); + expect(NavigationController.getEquipmentStatusText('shipped')).toBe('In transit'); + expect(NavigationController.getEquipmentStatusText('delivered')).toBe('Ready to activate'); + expect(NavigationController.getEquipmentStatusText('active')).toBe('Active'); + expect(NavigationController.getEquipmentStatusText('demo')).toBe('Demo mode'); + }); + }); + + describe('getEquipmentStatusColor', () => { + it('should handle undefined status', () => { + const result = NavigationController.getEquipmentStatusColor(undefined); + expect(result).toBe('#888888'); // gray + }); + + it('should return correct color for each status', () => { + expect(NavigationController.getEquipmentStatusColor('none')).toBe('#888888'); + expect(NavigationController.getEquipmentStatusColor('ordered')).toBe('#FFA500'); + expect(NavigationController.getEquipmentStatusColor('shipped')).toBe('#007AFF'); + expect(NavigationController.getEquipmentStatusColor('delivered')).toBe('#34C759'); + expect(NavigationController.getEquipmentStatusColor('active')).toBe('#34C759'); + expect(NavigationController.getEquipmentStatusColor('demo')).toBe('#9C27B0'); + }); + }); +});