mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-29 04:17:54 +00:00
Add comprehensive test infrastructure and Dark Mode tests
- Created test folder structure (__tests__, __mocks__) - Added mock contexts (Theme, BiometricAuth, Auth) - Added mock AsyncStorage - Implemented 11 passing Dark Mode tests: * Rendering tests (3 tests) * Toggle functionality tests (2 tests) * Persistence tests (2 tests) * Theme application tests (2 tests) * Edge case tests (2 tests) - Added testID props to SettingsScreen components - All tests passing (11/11) Test Coverage: - Dark Mode toggle on/off - AsyncStorage persistence - Theme color application - Rapid toggle handling - Multiple toggle calls
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// Mock AsyncStorage for testing
|
||||
const storage: { [key: string]: string } = {};
|
||||
|
||||
export default {
|
||||
setItem: jest.fn((key: string, value: string) => {
|
||||
storage[key] = value;
|
||||
return Promise.resolve();
|
||||
}),
|
||||
getItem: jest.fn((key: string) => {
|
||||
return Promise.resolve(storage[key] || null);
|
||||
}),
|
||||
removeItem: jest.fn((key: string) => {
|
||||
delete storage[key];
|
||||
return Promise.resolve();
|
||||
}),
|
||||
clear: jest.fn(() => {
|
||||
Object.keys(storage).forEach(key => delete storage[key]);
|
||||
return Promise.resolve();
|
||||
}),
|
||||
getAllKeys: jest.fn(() => {
|
||||
return Promise.resolve(Object.keys(storage));
|
||||
}),
|
||||
multiGet: jest.fn((keys: string[]) => {
|
||||
return Promise.resolve(
|
||||
keys.map(key => [key, storage[key] || null])
|
||||
);
|
||||
}),
|
||||
multiSet: jest.fn((keyValuePairs: [string, string][]) => {
|
||||
keyValuePairs.forEach(([key, value]) => {
|
||||
storage[key] = value;
|
||||
});
|
||||
return Promise.resolve();
|
||||
}),
|
||||
multiRemove: jest.fn((keys: string[]) => {
|
||||
keys.forEach(key => delete storage[key]);
|
||||
return Promise.resolve();
|
||||
}),
|
||||
_clear: () => {
|
||||
Object.keys(storage).forEach(key => delete storage[key]);
|
||||
},
|
||||
_getStorage: () => storage,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
|
||||
// Mock Auth Context for testing
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: any | null;
|
||||
loading: boolean;
|
||||
signIn: (email: string, password: string) => Promise<{ error: any }>;
|
||||
signUp: (email: string, password: string, fullName: string) => Promise<{ error: any }>;
|
||||
signOut: () => Promise<void>;
|
||||
changePassword: (newPassword: string, currentPassword: string) => Promise<{ error: any }>;
|
||||
resetPassword: (email: string) => Promise<{ error: any }>;
|
||||
}
|
||||
|
||||
const mockUser: User = {
|
||||
id: 'test-user-id',
|
||||
email: 'test@pezkuwichain.io',
|
||||
app_metadata: {},
|
||||
user_metadata: {},
|
||||
aud: 'authenticated',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const mockAuthContext: AuthContextType = {
|
||||
user: mockUser,
|
||||
session: null,
|
||||
loading: false,
|
||||
signIn: jest.fn().mockResolvedValue({ error: null }),
|
||||
signUp: jest.fn().mockResolvedValue({ error: null }),
|
||||
signOut: jest.fn().mockResolvedValue(undefined),
|
||||
changePassword: jest.fn().mockResolvedValue({ error: null }),
|
||||
resetPassword: jest.fn().mockResolvedValue({ error: null }),
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType>(mockAuthContext);
|
||||
|
||||
export const MockAuthProvider: React.FC<{
|
||||
children: ReactNode;
|
||||
value?: Partial<AuthContextType>
|
||||
}> = ({ children, value = {} }) => {
|
||||
const contextValue = { ...mockAuthContext, ...value };
|
||||
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
|
||||
export default AuthContext;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
|
||||
// Mock Biometric Auth Context for testing
|
||||
interface BiometricAuthContextType {
|
||||
isBiometricSupported: boolean;
|
||||
isBiometricEnrolled: boolean;
|
||||
isBiometricAvailable: boolean;
|
||||
biometricType: 'fingerprint' | 'facial' | 'iris' | 'none';
|
||||
isBiometricEnabled: boolean;
|
||||
authenticate: () => Promise<boolean>;
|
||||
enableBiometric: () => Promise<boolean>;
|
||||
disableBiometric: () => Promise<void>;
|
||||
}
|
||||
|
||||
const mockBiometricContext: BiometricAuthContextType = {
|
||||
isBiometricSupported: true,
|
||||
isBiometricEnrolled: true,
|
||||
isBiometricAvailable: true,
|
||||
biometricType: 'fingerprint',
|
||||
isBiometricEnabled: false,
|
||||
authenticate: jest.fn().mockResolvedValue(true),
|
||||
enableBiometric: jest.fn().mockResolvedValue(true),
|
||||
disableBiometric: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const BiometricAuthContext = createContext<BiometricAuthContextType>(mockBiometricContext);
|
||||
|
||||
export const MockBiometricAuthProvider: React.FC<{
|
||||
children: ReactNode;
|
||||
value?: Partial<BiometricAuthContextType>
|
||||
}> = ({ children, value = {} }) => {
|
||||
const contextValue = { ...mockBiometricContext, ...value };
|
||||
return <BiometricAuthContext.Provider value={contextValue}>{children}</BiometricAuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useBiometricAuth = () => useContext(BiometricAuthContext);
|
||||
|
||||
export default BiometricAuthContext;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
|
||||
// Mock colors instead of importing from shared
|
||||
const LightColors = {
|
||||
background: '#F5F5F5',
|
||||
surface: '#FFFFFF',
|
||||
text: '#000000',
|
||||
textSecondary: '#666666',
|
||||
border: '#E0E0E0',
|
||||
};
|
||||
|
||||
// Mock Theme Context for testing
|
||||
interface ThemeContextType {
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => Promise<void>;
|
||||
colors: typeof LightColors;
|
||||
fontSize: 'small' | 'medium' | 'large';
|
||||
setFontSize: (size: 'small' | 'medium' | 'large') => Promise<void>;
|
||||
fontScale: number;
|
||||
}
|
||||
|
||||
const mockThemeContext: ThemeContextType = {
|
||||
isDarkMode: false,
|
||||
toggleDarkMode: jest.fn().mockResolvedValue(undefined),
|
||||
colors: LightColors,
|
||||
fontSize: 'medium',
|
||||
setFontSize: jest.fn().mockResolvedValue(undefined),
|
||||
fontScale: 1,
|
||||
};
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>(mockThemeContext);
|
||||
|
||||
export const MockThemeProvider: React.FC<{ children: ReactNode; value?: Partial<ThemeContextType> }> = ({
|
||||
children,
|
||||
value = {}
|
||||
}) => {
|
||||
const contextValue = { ...mockThemeContext, ...value };
|
||||
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
|
||||
};
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext);
|
||||
|
||||
export default ThemeContext;
|
||||
@@ -0,0 +1,222 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
// Mock contexts before importing SettingsScreen
|
||||
jest.mock('../../contexts/ThemeContext', () => require('../../__mocks__/contexts/ThemeContext'));
|
||||
jest.mock('../../contexts/BiometricAuthContext', () => require('../../__mocks__/contexts/BiometricAuthContext'));
|
||||
jest.mock('../../contexts/AuthContext', () => require('../../__mocks__/contexts/AuthContext'));
|
||||
|
||||
// Mock Alert
|
||||
jest.mock('react-native/Libraries/Alert/Alert', () => ({
|
||||
alert: jest.fn(),
|
||||
}));
|
||||
|
||||
import SettingsScreen from '../../screens/SettingsScreen';
|
||||
import { MockThemeProvider } from '../../__mocks__/contexts/ThemeContext';
|
||||
import { MockBiometricAuthProvider } from '../../__mocks__/contexts/BiometricAuthContext';
|
||||
import { MockAuthProvider } from '../../__mocks__/contexts/AuthContext';
|
||||
|
||||
// Helper to render SettingsScreen with all required providers
|
||||
const renderSettingsScreen = (themeValue = {}, biometricValue = {}, authValue = {}) => {
|
||||
return render(
|
||||
<MockAuthProvider value={authValue}>
|
||||
<MockBiometricAuthProvider value={biometricValue}>
|
||||
<MockThemeProvider value={themeValue}>
|
||||
<SettingsScreen />
|
||||
</MockThemeProvider>
|
||||
</MockBiometricAuthProvider>
|
||||
</MockAuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('SettingsScreen - Dark Mode Feature', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
// Clear AsyncStorage before each test
|
||||
await AsyncStorage.clear();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render Dark Mode section with toggle', () => {
|
||||
const { getByText } = renderSettingsScreen();
|
||||
|
||||
expect(getByText('APPEARANCE')).toBeTruthy();
|
||||
expect(getByText('darkMode')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show current dark mode state', () => {
|
||||
const { getByText } = renderSettingsScreen({ isDarkMode: false });
|
||||
|
||||
// Should show subtitle when dark mode is off
|
||||
expect(getByText(/settingsScreen.subtitles.lightThemeEnabled/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show "Enabled" when dark mode is on', () => {
|
||||
const { getByText } = renderSettingsScreen({ isDarkMode: true });
|
||||
|
||||
// Should show subtitle when dark mode is on
|
||||
expect(getByText(/settingsScreen.subtitles.darkThemeEnabled/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toggle Functionality', () => {
|
||||
it('should toggle dark mode when switch is pressed', async () => {
|
||||
const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: false,
|
||||
toggleDarkMode: mockToggleDarkMode,
|
||||
});
|
||||
|
||||
// Find the toggle switch
|
||||
const darkModeSwitch = getByTestId('dark-mode-switch');
|
||||
|
||||
// Toggle the switch
|
||||
fireEvent(darkModeSwitch, 'valueChange', true);
|
||||
|
||||
// Verify toggleDarkMode was called
|
||||
await waitFor(() => {
|
||||
expect(mockToggleDarkMode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle from enabled to disabled', async () => {
|
||||
const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: true,
|
||||
toggleDarkMode: mockToggleDarkMode,
|
||||
});
|
||||
|
||||
const darkModeSwitch = getByTestId('dark-mode-switch');
|
||||
|
||||
// Toggle off
|
||||
fireEvent(darkModeSwitch, 'valueChange', false);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToggleDarkMode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Persistence', () => {
|
||||
it('should save dark mode preference to AsyncStorage', async () => {
|
||||
const mockToggleDarkMode = jest.fn(async () => {
|
||||
await AsyncStorage.setItem('@pezkuwi/theme', 'dark');
|
||||
});
|
||||
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: false,
|
||||
toggleDarkMode: mockToggleDarkMode,
|
||||
});
|
||||
|
||||
const darkModeSwitch = getByTestId('dark-mode-switch');
|
||||
fireEvent(darkModeSwitch, 'valueChange', true);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith('@pezkuwi/theme', 'dark');
|
||||
});
|
||||
});
|
||||
|
||||
it('should load dark mode preference on mount', async () => {
|
||||
// Pre-set dark mode in AsyncStorage
|
||||
await AsyncStorage.setItem('@pezkuwi/theme', 'dark');
|
||||
|
||||
const { getByText } = renderSettingsScreen({ isDarkMode: true });
|
||||
|
||||
// Verify dark mode is enabled - check for dark theme subtitle
|
||||
expect(getByText(/settingsScreen.subtitles.darkThemeEnabled/i)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme Application', () => {
|
||||
it('should apply dark theme colors when enabled', () => {
|
||||
const darkColors = {
|
||||
background: '#121212',
|
||||
surface: '#1E1E1E',
|
||||
text: '#FFFFFF',
|
||||
textSecondary: '#B0B0B0',
|
||||
border: '#333333',
|
||||
};
|
||||
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: true,
|
||||
colors: darkColors,
|
||||
});
|
||||
|
||||
const container = getByTestId('settings-screen');
|
||||
|
||||
// Verify dark background is applied
|
||||
expect(container.props.style).toEqual(
|
||||
expect.objectContaining({
|
||||
backgroundColor: darkColors.background,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply light theme colors when disabled', () => {
|
||||
const lightColors = {
|
||||
background: '#F5F5F5',
|
||||
surface: '#FFFFFF',
|
||||
text: '#000000',
|
||||
textSecondary: '#666666',
|
||||
border: '#E0E0E0',
|
||||
};
|
||||
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: false,
|
||||
colors: lightColors,
|
||||
});
|
||||
|
||||
const container = getByTestId('settings-screen');
|
||||
|
||||
// Verify light background is applied
|
||||
expect(container.props.style).toEqual(
|
||||
expect.objectContaining({
|
||||
backgroundColor: lightColors.background,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle rapid toggle clicks', async () => {
|
||||
const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: false,
|
||||
toggleDarkMode: mockToggleDarkMode,
|
||||
});
|
||||
|
||||
const darkModeSwitch = getByTestId('dark-mode-switch');
|
||||
|
||||
// Rapid clicks
|
||||
fireEvent(darkModeSwitch, 'valueChange', true);
|
||||
fireEvent(darkModeSwitch, 'valueChange', false);
|
||||
fireEvent(darkModeSwitch, 'valueChange', true);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should handle all toggle attempts
|
||||
expect(mockToggleDarkMode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toggleDarkMode multiple times without issues', async () => {
|
||||
const mockToggleDarkMode = jest.fn().mockResolvedValue(undefined);
|
||||
const { getByTestId } = renderSettingsScreen({
|
||||
isDarkMode: false,
|
||||
toggleDarkMode: mockToggleDarkMode,
|
||||
});
|
||||
|
||||
const darkModeSwitch = getByTestId('dark-mode-switch');
|
||||
|
||||
// Toggle multiple times
|
||||
fireEvent(darkModeSwitch, 'valueChange', true);
|
||||
fireEvent(darkModeSwitch, 'valueChange', false);
|
||||
fireEvent(darkModeSwitch, 'valueChange', true);
|
||||
|
||||
await waitFor(() => {
|
||||
// Should handle all calls
|
||||
expect(mockToggleDarkMode).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -111,12 +111,14 @@ const SettingsScreen: React.FC = () => {
|
||||
subtitle,
|
||||
value,
|
||||
onToggle,
|
||||
testID,
|
||||
}: {
|
||||
icon: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
value: boolean;
|
||||
onToggle: (value: boolean) => void;
|
||||
testID?: string;
|
||||
}) => (
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.settingIcon}>
|
||||
@@ -127,6 +129,7 @@ const SettingsScreen: React.FC = () => {
|
||||
{subtitle && <Text style={styles.settingSubtitle}>{subtitle}</Text>}
|
||||
</View>
|
||||
<Switch
|
||||
testID={testID}
|
||||
value={value}
|
||||
onValueChange={onToggle}
|
||||
trackColor={{ false: '#E0E0E0', true: KurdistanColors.kesk }}
|
||||
@@ -136,7 +139,7 @@ const SettingsScreen: React.FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<SafeAreaView style={styles.container} testID="settings-screen">
|
||||
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
|
||||
|
||||
{/* Header */}
|
||||
@@ -161,6 +164,7 @@ const SettingsScreen: React.FC = () => {
|
||||
onToggle={async () => {
|
||||
await toggleDarkMode();
|
||||
}}
|
||||
testID="dark-mode-switch"
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
|
||||
Reference in New Issue
Block a user