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
15 KiB
TypeScript
425 lines
15 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|
|
});
|