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.
This commit is contained in:
parent
7d9e7e37bf
commit
deddd3d5bc
424
hooks/__tests__/useNavigationFlow.nullsafety.test.tsx
Normal file
424
hooks/__tests__/useNavigationFlow.nullsafety.test.tsx
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -21,24 +21,34 @@ export function useNavigationFlow() {
|
|||||||
/**
|
/**
|
||||||
* Execute navigation based on NavigationResult
|
* 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
|
const href = result.params
|
||||||
? { pathname: result.path, params: result.params }
|
? { pathname: result.path, params: result.params }
|
||||||
: result.path;
|
: result.path;
|
||||||
|
|
||||||
|
try {
|
||||||
if (replace) {
|
if (replace) {
|
||||||
router.replace(href as any);
|
router.replace(href as any);
|
||||||
} else {
|
} else {
|
||||||
router.push(href as any);
|
router.push(href as any);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[useNavigationFlow] Navigation error:', error);
|
||||||
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate after successful login/OTP verification
|
* Navigate after successful login/OTP verification
|
||||||
*/
|
*/
|
||||||
const navigateAfterLogin = useCallback((
|
const navigateAfterLogin = useCallback((
|
||||||
profile: UserProfile | null,
|
profile: UserProfile | null | undefined,
|
||||||
beneficiaries: Beneficiary[]
|
beneficiaries: Beneficiary[] | null | undefined
|
||||||
) => {
|
) => {
|
||||||
const result = NavigationController.getRouteAfterLogin(profile, beneficiaries);
|
const result = NavigationController.getRouteAfterLogin(profile, beneficiaries);
|
||||||
navigate(result, true); // replace to prevent going back to login
|
navigate(result, true); // replace to prevent going back to login
|
||||||
@ -48,7 +58,7 @@ export function useNavigationFlow() {
|
|||||||
* Navigate after creating a new beneficiary
|
* Navigate after creating a new beneficiary
|
||||||
*/
|
*/
|
||||||
const navigateAfterAddBeneficiary = useCallback((
|
const navigateAfterAddBeneficiary = useCallback((
|
||||||
beneficiaryId: number,
|
beneficiaryId: number | null | undefined,
|
||||||
hasExistingDevices: boolean
|
hasExistingDevices: boolean
|
||||||
) => {
|
) => {
|
||||||
const result = NavigationController.getRouteAfterAddBeneficiary(
|
const result = NavigationController.getRouteAfterAddBeneficiary(
|
||||||
@ -62,8 +72,8 @@ export function useNavigationFlow() {
|
|||||||
* Navigate after purchase completion
|
* Navigate after purchase completion
|
||||||
*/
|
*/
|
||||||
const navigateAfterPurchase = useCallback((
|
const navigateAfterPurchase = useCallback((
|
||||||
beneficiaryId: number,
|
beneficiaryId: number | null | undefined,
|
||||||
options: { skipToActivate?: boolean; demo?: boolean } = {}
|
options: { skipToActivate?: boolean; demo?: boolean } | null | undefined = {}
|
||||||
) => {
|
) => {
|
||||||
const result = NavigationController.getRouteAfterPurchase(beneficiaryId, options);
|
const result = NavigationController.getRouteAfterPurchase(beneficiaryId, options);
|
||||||
navigate(result);
|
navigate(result);
|
||||||
@ -72,7 +82,7 @@ export function useNavigationFlow() {
|
|||||||
/**
|
/**
|
||||||
* Navigate after device activation
|
* Navigate after device activation
|
||||||
*/
|
*/
|
||||||
const navigateAfterActivation = useCallback((beneficiaryId: number) => {
|
const navigateAfterActivation = useCallback((beneficiaryId: number | null | undefined) => {
|
||||||
const result = NavigationController.getRouteAfterActivation(beneficiaryId);
|
const result = NavigationController.getRouteAfterActivation(beneficiaryId);
|
||||||
navigate(result, true); // replace to prevent going back
|
navigate(result, true); // replace to prevent going back
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@ -81,7 +91,7 @@ export function useNavigationFlow() {
|
|||||||
* Navigate to beneficiary-specific screens
|
* Navigate to beneficiary-specific screens
|
||||||
*/
|
*/
|
||||||
const navigateToBeneficiary = useCallback((
|
const navigateToBeneficiary = useCallback((
|
||||||
beneficiary: Beneficiary,
|
beneficiary: Beneficiary | null | undefined,
|
||||||
action: 'view' | 'subscription' | 'equipment' | 'share' = 'view'
|
action: 'view' | 'subscription' | 'equipment' | 'share' = 'view'
|
||||||
) => {
|
) => {
|
||||||
const result = NavigationController.getBeneficiaryRoute(beneficiary, action);
|
const result = NavigationController.getBeneficiaryRoute(beneficiary, action);
|
||||||
@ -91,7 +101,7 @@ export function useNavigationFlow() {
|
|||||||
/**
|
/**
|
||||||
* Navigate to beneficiary setup flow (purchase or activate)
|
* 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);
|
const result = NavigationController.getRouteForBeneficiarySetup(beneficiary);
|
||||||
navigate(result);
|
navigate(result);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@ -122,14 +132,26 @@ export function useNavigationFlow() {
|
|||||||
navigate({ path: ROUTES.AUTH.ADD_LOVED_ONE });
|
navigate({ path: ROUTES.AUTH.ADD_LOVED_ONE });
|
||||||
}, [navigate]);
|
}, [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({
|
navigate({
|
||||||
path: ROUTES.AUTH.PURCHASE,
|
path: ROUTES.AUTH.PURCHASE,
|
||||||
params: { beneficiaryId },
|
params: { beneficiaryId },
|
||||||
});
|
});
|
||||||
}, [navigate]);
|
}, [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({
|
navigate({
|
||||||
path: ROUTES.AUTH.ACTIVATE,
|
path: ROUTES.AUTH.ACTIVATE,
|
||||||
params: demo !== undefined ? { beneficiaryId, demo } : { beneficiaryId },
|
params: demo !== undefined ? { beneficiaryId, demo } : { beneficiaryId },
|
||||||
|
|||||||
@ -86,8 +86,8 @@ export const NavigationController = {
|
|||||||
* 4. User has active beneficiary → dashboard
|
* 4. User has active beneficiary → dashboard
|
||||||
*/
|
*/
|
||||||
getRouteAfterLogin(
|
getRouteAfterLogin(
|
||||||
profile: UserProfile | null,
|
profile: UserProfile | null | undefined,
|
||||||
beneficiaries: Beneficiary[]
|
beneficiaries: Beneficiary[] | null | undefined
|
||||||
): NavigationResult {
|
): NavigationResult {
|
||||||
// Step 1: Check if user has name
|
// Step 1: Check if user has name
|
||||||
if (!profile?.firstName) {
|
if (!profile?.firstName) {
|
||||||
@ -112,6 +112,14 @@ export const NavigationController = {
|
|||||||
|
|
||||||
// Step 4: Single beneficiary - check if needs setup
|
// Step 4: Single beneficiary - check if needs setup
|
||||||
const singleBeneficiary = beneficiaries[0];
|
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 ||
|
const isActive = singleBeneficiary.hasDevices ||
|
||||||
singleBeneficiary.equipmentStatus === 'active' ||
|
singleBeneficiary.equipmentStatus === 'active' ||
|
||||||
singleBeneficiary.equipmentStatus === 'demo';
|
singleBeneficiary.equipmentStatus === 'demo';
|
||||||
@ -130,7 +138,14 @@ export const NavigationController = {
|
|||||||
/**
|
/**
|
||||||
* Determine where to navigate for beneficiary equipment setup
|
* 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';
|
const status = beneficiary.equipmentStatus || 'none';
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -179,9 +194,16 @@ export const NavigationController = {
|
|||||||
* @param hasExistingDevices - User indicated they already have WellNuo devices
|
* @param hasExistingDevices - User indicated they already have WellNuo devices
|
||||||
*/
|
*/
|
||||||
getRouteAfterAddBeneficiary(
|
getRouteAfterAddBeneficiary(
|
||||||
beneficiaryId: number,
|
beneficiaryId: number | null | undefined,
|
||||||
hasExistingDevices: boolean
|
hasExistingDevices: boolean
|
||||||
): NavigationResult {
|
): NavigationResult {
|
||||||
|
// Null safety: validate beneficiary ID
|
||||||
|
if (!beneficiaryId || typeof beneficiaryId !== 'number') {
|
||||||
|
return {
|
||||||
|
path: ROUTES.AUTH.ADD_LOVED_ONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (hasExistingDevices) {
|
if (hasExistingDevices) {
|
||||||
// User has existing devices - go directly to activation
|
// User has existing devices - go directly to activation
|
||||||
return {
|
return {
|
||||||
@ -201,10 +223,17 @@ export const NavigationController = {
|
|||||||
* After successful purchase, navigate to next step
|
* After successful purchase, navigate to next step
|
||||||
*/
|
*/
|
||||||
getRouteAfterPurchase(
|
getRouteAfterPurchase(
|
||||||
beneficiaryId: number,
|
beneficiaryId: number | null | undefined,
|
||||||
purchaseResult: { skipToActivate?: boolean; demo?: boolean }
|
purchaseResult: { skipToActivate?: boolean; demo?: boolean } | null | undefined
|
||||||
): NavigationResult {
|
): 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
|
// Demo mode or skip - go to activate
|
||||||
return {
|
return {
|
||||||
path: ROUTES.AUTH.ACTIVATE,
|
path: ROUTES.AUTH.ACTIVATE,
|
||||||
@ -225,7 +254,14 @@ export const NavigationController = {
|
|||||||
/**
|
/**
|
||||||
* After successful activation, navigate to beneficiary detail page
|
* 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 {
|
return {
|
||||||
path: ROUTES.BENEFICIARY.DETAIL(beneficiaryId),
|
path: ROUTES.BENEFICIARY.DETAIL(beneficiaryId),
|
||||||
params: { justActivated: true },
|
params: { justActivated: true },
|
||||||
@ -238,8 +274,8 @@ export const NavigationController = {
|
|||||||
* Used when user logs in with existing account
|
* Used when user logs in with existing account
|
||||||
*/
|
*/
|
||||||
getRouteForReturningUser(
|
getRouteForReturningUser(
|
||||||
profile: UserProfile,
|
profile: UserProfile | null | undefined,
|
||||||
beneficiaries: Beneficiary[]
|
beneficiaries: Beneficiary[] | null | undefined
|
||||||
): NavigationResult {
|
): NavigationResult {
|
||||||
// Same logic as after login, but could add welcome-back screen
|
// Same logic as after login, but could add welcome-back screen
|
||||||
return this.getRouteAfterLogin(profile, beneficiaries);
|
return this.getRouteAfterLogin(profile, beneficiaries);
|
||||||
@ -259,9 +295,16 @@ export const NavigationController = {
|
|||||||
* Get route for a specific beneficiary action
|
* Get route for a specific beneficiary action
|
||||||
*/
|
*/
|
||||||
getBeneficiaryRoute(
|
getBeneficiaryRoute(
|
||||||
beneficiary: Beneficiary,
|
beneficiary: Beneficiary | null | undefined,
|
||||||
action: 'view' | 'subscription' | 'equipment' | 'share'
|
action: 'view' | 'subscription' | 'equipment' | 'share'
|
||||||
): NavigationResult {
|
): NavigationResult {
|
||||||
|
// Null safety: validate beneficiary exists and has valid id
|
||||||
|
if (!beneficiary || typeof beneficiary.id !== 'number') {
|
||||||
|
return {
|
||||||
|
path: ROUTES.TABS.BENEFICIARIES,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'view':
|
case 'view':
|
||||||
return { path: ROUTES.BENEFICIARY.DETAIL(beneficiary.id) };
|
return { path: ROUTES.BENEFICIARY.DETAIL(beneficiary.id) };
|
||||||
@ -279,7 +322,12 @@ export const NavigationController = {
|
|||||||
/**
|
/**
|
||||||
* Determine if beneficiary card should show "Setup Equipment" button
|
* 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 &&
|
return !beneficiary.hasDevices &&
|
||||||
beneficiary.equipmentStatus !== 'active' &&
|
beneficiary.equipmentStatus !== 'active' &&
|
||||||
beneficiary.equipmentStatus !== 'demo';
|
beneficiary.equipmentStatus !== 'demo';
|
||||||
@ -289,8 +337,13 @@ export const NavigationController = {
|
|||||||
* Get call-to-action for beneficiary based on status
|
* Get call-to-action for beneficiary based on status
|
||||||
*/
|
*/
|
||||||
getBeneficiaryCallToAction(
|
getBeneficiaryCallToAction(
|
||||||
beneficiary: Beneficiary
|
beneficiary: Beneficiary | null | undefined
|
||||||
): { label: string; action: () => NavigationResult } | null {
|
): { 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';
|
const status = beneficiary.equipmentStatus || 'none';
|
||||||
|
|
||||||
if (beneficiary.hasDevices || status === 'active' || status === 'demo') {
|
if (beneficiary.hasDevices || status === 'active' || status === 'demo') {
|
||||||
|
|||||||
424
services/__tests__/NavigationController.nullsafety.test.ts
Normal file
424
services/__tests__/NavigationController.nullsafety.test.ts
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user