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.
425 lines
11 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|