import { render, waitFor } from '@testing-library/react-native'; import React from 'react'; import * as SplashScreen from 'expo-splash-screen'; import { router, useRouter, useRootNavigationState, useSegments } from 'expo-router'; import RootLayout from '@/app/_layout'; import { useAuth } from '@/contexts/AuthContext'; import { useBLE } from '@/contexts/BLEContext'; // Mock react-native-reanimated jest.mock('react-native-reanimated', () => ({ default: {}, useSharedValue: jest.fn, useAnimatedStyle: jest.fn, withTiming: jest.fn, withSpring: jest.fn, withDecay: jest.fn, withDelay: jest.fn, withRepeat: jest.fn, withSequence: jest.fn, cancelAnimation: jest.fn, measure: jest.fn, Easing: {}, Extrapolate: {}, runOnJS: jest.fn, runOnUI: jest.fn, })); // Mock dependencies jest.mock('expo-router', () => { const mockRouter = { replace: jest.fn(), push: jest.fn(), }; const Stack = ({ children }: { children: React.ReactNode }) => <>{children}; (Stack as any).Screen = () => null; return { Stack, router: mockRouter, useRouter: jest.fn(() => mockRouter), useRootNavigationState: jest.fn(), useSegments: jest.fn(), }; }); jest.mock('expo-splash-screen', () => ({ preventAutoHideAsync: jest.fn().mockResolvedValue(undefined), hideAsync: jest.fn().mockResolvedValue(undefined), })); jest.mock('@/contexts/AuthContext', () => ({ AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}, useAuth: jest.fn(), })); jest.mock('@/contexts/BeneficiaryContext', () => ({ BeneficiaryProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); jest.mock('@/contexts/BLEContext', () => ({ BLEProvider: ({ children }: { children: React.ReactNode }) => <>{children}, useBLE: jest.fn(), })); jest.mock('@/hooks/use-color-scheme', () => ({ useColorScheme: jest.fn(() => 'light'), })); jest.mock('@stripe/stripe-react-native', () => ({ StripeProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); jest.mock('@/components/ui/Toast', () => ({ ToastProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); jest.mock('@/components/ui/LoadingSpinner', () => { const { Text } = require('react-native'); return { LoadingSpinner: ({ message }: { message?: string; fullScreen?: boolean }) => ( {message || 'Loading'} ), }; }); describe('RootLayout Redirect Logic', () => { const mockNavigationState = { key: 'root' }; beforeEach(() => { jest.clearAllMocks(); // Reset splash screen mocks (SplashScreen.hideAsync as jest.Mock).mockClear(); (SplashScreen.preventAutoHideAsync as jest.Mock).mockClear(); (useRootNavigationState as jest.Mock).mockReturnValue(mockNavigationState); (useBLE as jest.Mock).mockReturnValue({ cleanupBLE: jest.fn(), }); }); describe('Initial redirect - Not authenticated', () => { it('should redirect to login when not authenticated and not in auth group', async () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: false, isInitializing: false, user: null, }); (useSegments as jest.Mock).mockReturnValue(['(tabs)', 'dashboard']); render(); await waitFor(() => { expect(router.replace).toHaveBeenCalledWith('/(auth)/login'); }); }); it('should not redirect when not authenticated but already in auth group', async () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: false, isInitializing: false, user: null, }); (useSegments as jest.Mock).mockReturnValue(['(auth)', 'login']); render(); await waitFor(() => { expect(router.replace).not.toHaveBeenCalled(); }); }); }); describe('Initial redirect - Authenticated', () => { it('should not redirect when authenticated and in auth group (login flow)', async () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: true, isInitializing: false, user: { user_id: 1, email: 'test@example.com', firstName: 'John' }, }); (useSegments as jest.Mock).mockReturnValue(['(auth)', 'verify-otp']); render(); await waitFor(() => { expect(router.replace).not.toHaveBeenCalled(); }); }); it('should redirect to enter-name when authenticated user has no firstName', async () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: true, isInitializing: false, user: { user_id: 1, email: 'test@example.com', firstName: null }, }); (useSegments as jest.Mock).mockReturnValue(['(tabs)', 'dashboard']); render(); await waitFor(() => { expect(router.replace).toHaveBeenCalledWith('/(auth)/enter-name'); }); }); it('should not redirect when authenticated user has firstName and in tabs', async () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: true, isInitializing: false, user: { user_id: 1, email: 'test@example.com', firstName: 'John' }, }); (useSegments as jest.Mock).mockReturnValue(['(tabs)', 'dashboard']); render(); await waitFor(() => { expect(router.replace).not.toHaveBeenCalled(); }); }); }); describe('Ongoing protection', () => { it('should redirect to login when user logs out while in tabs', async () => { const { rerender } = render(); // Initial state: authenticated (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: true, isInitializing: false, user: { user_id: 1, email: 'test@example.com', firstName: 'John' }, }); (useSegments as jest.Mock).mockReturnValue(['(tabs)', 'dashboard']); rerender(); // Clear previous calls (router.replace as jest.Mock).mockClear(); // Logout happens (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: false, isInitializing: false, user: null, }); rerender(); await waitFor(() => { expect(router.replace).toHaveBeenCalledWith('/(auth)/login'); }); }); }); describe('Loading states', () => { it('should show loading spinner while initializing', () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: false, isInitializing: true, user: null, }); (useSegments as jest.Mock).mockReturnValue([]); const { getByText } = render(); expect(getByText('Loading...')).toBeTruthy(); }); it('should not redirect while navigation is not ready', async () => { (useAuth as jest.Mock).mockReturnValue({ isAuthenticated: false, isInitializing: false, user: null, }); (useRootNavigationState as jest.Mock).mockReturnValue(null); (useSegments as jest.Mock).mockReturnValue(['(tabs)']); render(); await waitFor(() => { expect(router.replace).not.toHaveBeenCalled(); }); }); }); });