WellNuo/hooks/__tests__/useNavigationFlow.nullsafety.test.tsx
Sergei deddd3d5bc 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.
2026-01-29 12:05:29 -08:00

425 lines
11 KiB
TypeScript

/**
* 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);
});
});
});