Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions __mocks__/@react-native-async-storage/async-storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const mockStorage = {};

const AsyncStorage = {
setItem: jest.fn((key, value) => {
mockStorage[key] = value;
return Promise.resolve();
}),
getItem: jest.fn((key) => {
return Promise.resolve(mockStorage[key] || null);
}),
removeItem: jest.fn((key) => {
delete mockStorage[key];
return Promise.resolve();
}),
multiGet: jest.fn((keys) => {
const result = keys.map((key) => [key, mockStorage[key] || null]);
return Promise.resolve(result);
}),
multiSet: jest.fn((keyValuePairs) => {
keyValuePairs.forEach(([key, value]) => {
mockStorage[key] = value;
});
return Promise.resolve();
}),
multiRemove: jest.fn((keys) => {
keys.forEach((key) => {
delete mockStorage[key];
});
return Promise.resolve();
}),
clear: jest.fn(() => {
Object.keys(mockStorage).forEach((key) => {
delete mockStorage[key];
});
return Promise.resolve();
}),
getAllKeys: jest.fn(() => {
return Promise.resolve(Object.keys(mockStorage));
}),
__resetMockStorage: () => {
Object.keys(mockStorage).forEach((key) => {
delete mockStorage[key];
});
AsyncStorage.setItem.mockClear();
AsyncStorage.getItem.mockClear();
AsyncStorage.removeItem.mockClear();
AsyncStorage.multiGet.mockClear();
AsyncStorage.multiSet.mockClear();
AsyncStorage.multiRemove.mockClear();
AsyncStorage.clear.mockClear();
AsyncStorage.getAllKeys.mockClear();
},
__getMockStorage: () => ({ ...mockStorage }),
__setMockStorage: (data) => {
Object.keys(mockStorage).forEach((key) => {
delete mockStorage[key];
});
Object.assign(mockStorage, data);
},
};

export default AsyncStorage;
44 changes: 44 additions & 0 deletions __mocks__/@react-navigation/native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';

export const DarkTheme = {
dark: true,
colors: {
primary: '#0a7ea4',
background: '#000',
card: '#1c1c1e',
text: '#fff',
border: '#272729',
notification: '#ff453a',
},
};

export const DefaultTheme = {
dark: false,
colors: {
primary: '#0a7ea4',
background: '#fff',
card: '#fff',
text: '#000',
border: '#d8d8d8',
notification: '#ff3b30',
},
};

export const ThemeProvider = ({ children }) => children;

export const useNavigation = jest.fn(() => ({
navigate: jest.fn(),
goBack: jest.fn(),
setOptions: jest.fn(),
}));

export const useRoute = jest.fn(() => ({
params: {},
name: 'Test',
}));

export const useFocusEffect = jest.fn((callback) => {
callback();
});

export const NavigationContainer = ({ children }) => children;
16 changes: 16 additions & 0 deletions __mocks__/expo-font.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
let mockLoaded = true;

export const useFonts = jest.fn(() => [mockLoaded]);

export const loadAsync = jest.fn(() => Promise.resolve());

// Test helpers
export const __setFontsLoaded = (loaded) => {
mockLoaded = loaded;
};

export const __resetMocks = () => {
mockLoaded = true;
useFonts.mockClear();
loadAsync.mockClear();
};
33 changes: 33 additions & 0 deletions __mocks__/expo-router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const mockRouter = {
push: jest.fn(),
replace: jest.fn(),
back: jest.fn(),
canGoBack: jest.fn(() => false),
setParams: jest.fn(),
};

let mockSegments = [];

export const useRouter = jest.fn(() => mockRouter);

export const useSegments = jest.fn(() => mockSegments);

export const Slot = ({ children }) => children || null;

export const Link = ({ children }) => children;

export const router = mockRouter;

// Test helpers
export const __setMockSegments = (segments) => {
mockSegments = segments;
};

export const __resetMocks = () => {
mockSegments = [];
mockRouter.push.mockClear();
mockRouter.replace.mockClear();
mockRouter.back.mockClear();
mockRouter.canGoBack.mockClear();
mockRouter.setParams.mockClear();
};
6 changes: 6 additions & 0 deletions __mocks__/expo-status-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const StatusBar = () => null;

export const setStatusBarStyle = jest.fn();
export const setStatusBarHidden = jest.fn();
export const setStatusBarBackgroundColor = jest.fn();
export const setStatusBarTranslucent = jest.fn();
99 changes: 99 additions & 0 deletions __tests__/app/home.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react-native';
import HomeScreen from '@/app/(tabs)/index';
import { AuthProvider } from '@/context/AuthContext';
import * as authService from '@/services/authService';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createMockUser } from '../test-utils';

jest.mock('@/services/authService');

const mockAuthService = authService as jest.Mocked<typeof authService>;
const mockAsyncStorage = AsyncStorage as jest.Mocked<typeof AsyncStorage> & {
__resetMockStorage: () => void;
};

function renderHomeScreen() {
return render(
<AuthProvider>
<HomeScreen />
</AuthProvider>
);
}

describe('HomeScreen', () => {
beforeEach(() => {
jest.clearAllMocks();
mockAsyncStorage.__resetMockStorage();
});

describe('rendering with authenticated user', () => {
beforeEach(() => {
const mockUser = createMockUser({ displayName: 'John Doe' });
mockAuthService.getStoredTokens.mockResolvedValue({
accessToken: 'token',
refreshToken: 'refresh',
user: mockUser,
});
});

it('should display welcome message with user displayName', async () => {
renderHomeScreen();

await waitFor(() => {
expect(screen.getByText('Welcome, John Doe!')).toBeTruthy();
});
});

it('should render logout button', async () => {
renderHomeScreen();

await waitFor(() => {
expect(screen.getByText('Logout')).toBeTruthy();
});
});

it('should call logout when logout button is pressed', async () => {
mockAuthService.logout.mockResolvedValue(undefined);

renderHomeScreen();

await waitFor(() => {
expect(screen.getByText('Logout')).toBeTruthy();
});

await act(async () => {
fireEvent.press(screen.getByText('Logout'));
});

expect(mockAuthService.logout).toHaveBeenCalled();
});
});

describe('rendering with null user', () => {
beforeEach(() => {
mockAuthService.getStoredTokens.mockResolvedValue({
accessToken: null,
refreshToken: null,
user: null,
});
});

it('should handle null user gracefully', async () => {
renderHomeScreen();

await waitFor(() => {
// Should render "Welcome, !" since user?.displayName is undefined
expect(screen.getByText(/Welcome/)).toBeTruthy();
});
});

it('should still render logout button when user is null', async () => {
renderHomeScreen();

await waitFor(() => {
expect(screen.getByText('Logout')).toBeTruthy();
});
});
});
});
Loading