/** * ThemeContext Tests * * Tests for dark mode support functionality */ import React from 'react'; import { renderHook, act, waitFor } from '@testing-library/react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { ThemeProvider, useTheme, useThemeOptional, type ThemeMode } from '../ThemeContext'; // Mock AsyncStorage jest.mock('@react-native-async-storage/async-storage', () => ({ getItem: jest.fn(() => Promise.resolve(null)), setItem: jest.fn(() => Promise.resolve()), })); // Mock useColorScheme hook via the hook path let mockSystemColorScheme: 'light' | 'dark' | null = 'light'; jest.mock('react-native/Libraries/Utilities/useColorScheme', () => ({ default: () => mockSystemColorScheme, })); describe('ThemeContext', () => { beforeEach(() => { jest.clearAllMocks(); mockSystemColorScheme = 'light'; (AsyncStorage.getItem as jest.Mock).mockResolvedValue(null); }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); describe('ThemeProvider', () => { it('should provide default theme values', async () => { const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('system'); expect(result.current.resolvedTheme).toBe('light'); expect(result.current.isDark).toBe(false); }); }); it('should resolve to dark theme when system is dark', async () => { mockSystemColorScheme = 'dark'; const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.resolvedTheme).toBe('dark'); expect(result.current.isDark).toBe(true); }); }); it('should load saved theme preference from storage', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('dark'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('dark'); expect(result.current.resolvedTheme).toBe('dark'); expect(result.current.isDark).toBe(true); }); }); it('should load light theme preference from storage', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('light'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('light'); expect(result.current.resolvedTheme).toBe('light'); expect(result.current.isDark).toBe(false); }); }); }); describe('setThemeMode', () => { it('should change theme mode to dark', async () => { const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('system'); }); await act(async () => { await result.current.setThemeMode('dark'); }); expect(result.current.themeMode).toBe('dark'); expect(result.current.resolvedTheme).toBe('dark'); expect(result.current.isDark).toBe(true); expect(AsyncStorage.setItem).toHaveBeenCalledWith('@wellnuo:theme_preference', 'dark'); }); it('should change theme mode to light', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('dark'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('dark'); }); await act(async () => { await result.current.setThemeMode('light'); }); expect(result.current.themeMode).toBe('light'); expect(result.current.resolvedTheme).toBe('light'); expect(result.current.isDark).toBe(false); expect(AsyncStorage.setItem).toHaveBeenCalledWith('@wellnuo:theme_preference', 'light'); }); it('should change theme mode to system', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('dark'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('dark'); }); await act(async () => { await result.current.setThemeMode('system'); }); expect(result.current.themeMode).toBe('system'); expect(result.current.resolvedTheme).toBe('light'); // system is light expect(AsyncStorage.setItem).toHaveBeenCalledWith('@wellnuo:theme_preference', 'system'); }); }); describe('toggleTheme', () => { it('should toggle from light to dark', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('light'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.resolvedTheme).toBe('light'); }); await act(async () => { await result.current.toggleTheme(); }); expect(result.current.themeMode).toBe('dark'); expect(result.current.resolvedTheme).toBe('dark'); }); it('should toggle from dark to light', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('dark'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.resolvedTheme).toBe('dark'); }); await act(async () => { await result.current.toggleTheme(); }); expect(result.current.themeMode).toBe('light'); expect(result.current.resolvedTheme).toBe('light'); }); it('should toggle from system (light) to dark', async () => { mockSystemColorScheme = 'light'; const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('system'); expect(result.current.resolvedTheme).toBe('light'); }); await act(async () => { await result.current.toggleTheme(); }); expect(result.current.themeMode).toBe('dark'); expect(result.current.resolvedTheme).toBe('dark'); }); it('should toggle from system (dark) to light', async () => { mockSystemColorScheme = 'dark'; const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('system'); expect(result.current.resolvedTheme).toBe('dark'); }); await act(async () => { await result.current.toggleTheme(); }); expect(result.current.themeMode).toBe('light'); expect(result.current.resolvedTheme).toBe('light'); }); }); describe('useThemeOptional', () => { it('should return null when used outside provider', () => { const { result } = renderHook(() => useThemeOptional()); expect(result.current).toBeNull(); }); it('should return theme context when used inside provider', async () => { const { result } = renderHook(() => useThemeOptional(), { wrapper }); await waitFor(() => { expect(result.current).not.toBeNull(); expect(result.current?.themeMode).toBe('system'); }); }); }); describe('useTheme error handling', () => { it('should throw error when used outside provider', () => { // Suppress console.error for this test const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); expect(() => { renderHook(() => useTheme()); }).toThrow('useTheme must be used within a ThemeProvider'); consoleSpy.mockRestore(); }); }); describe('error handling', () => { it('should handle AsyncStorage read error gracefully', async () => { const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); (AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error')); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { // Should fallback to default 'system' mode expect(result.current.themeMode).toBe('system'); }); consoleSpy.mockRestore(); }); it('should handle AsyncStorage write error gracefully', async () => { const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); (AsyncStorage.setItem as jest.Mock).mockRejectedValue(new Error('Storage error')); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { expect(result.current.themeMode).toBe('system'); }); // Should still update state even if storage fails await act(async () => { await result.current.setThemeMode('dark'); }); expect(result.current.themeMode).toBe('dark'); consoleSpy.mockRestore(); }); it('should ignore invalid stored theme values', async () => { (AsyncStorage.getItem as jest.Mock).mockResolvedValue('invalid_theme'); const { result } = renderHook(() => useTheme(), { wrapper }); await waitFor(() => { // Should fallback to default 'system' mode expect(result.current.themeMode).toBe('system'); }); }); }); });