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();
});
});
});
});