Skip to content
Merged
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
10 changes: 5 additions & 5 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
module.exports = {
testEnvironment: 'node',
preset: 'jest-expo',
roots: ['<rootDir>/src', '<rootDir>/tests'],
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: { jsx: 'react-jsx' } }],
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|tsx)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
Expand All @@ -15,8 +12,11 @@ module.exports = {
'^@store/(.*)$': '<rootDir>/src/store/$1',
'^@types/(.*)$': '<rootDir>/src/types/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^react-native$': '<rootDir>/node_modules/react-native',
},
transformIgnorePatterns: [
// Transform all expo-* packages and other native modules
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(-.*)?|@expo(-.*)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)',
],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
Expand Down
189 changes: 152 additions & 37 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Mock react-native
// Global mock for react-native to support tests
jest.mock('react-native', () => ({
Platform: { OS: 'ios', select: jest.fn((obj) => obj.ios) },
Linking: {
Expand All @@ -12,20 +12,140 @@ jest.mock('react-native', () => ({
SafeAreaView: 'SafeAreaView',
ScrollView: 'ScrollView',
Switch: 'Switch',
TextInput: 'TextInput',
ActivityIndicator: 'ActivityIndicator',
Image: 'Image',
StyleSheet: {
create: (styles) => styles,
flatten: (style) => (style ? (Array.isArray(style) ? Object.assign({}, ...style) : style) : {}),
hairlineWidth: 1,
absoluteFill: {},
absoluteFillObject: {},
},
useWindowDimensions: () => ({ width: 390, height: 844, fontScale: 1, scale: 1 }),
useColorScheme: () => 'light',
Appearance: {
getColorScheme: () => 'light',
addChangeListener: jest.fn(() => ({ remove: jest.fn() })),
removeChangeListener: jest.fn(),
},
AppState: {
currentState: 'active',
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
},
Dimensions: {
get: () => ({ width: 390, height: 844 }),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
},
Animated: {
View: 'Animated.View',
Text: 'Animated.Text',
Image: 'Animated.Image',
Value: jest.fn(() => ({
setValue: jest.fn(),
interpolate: jest.fn(),
addListener: jest.fn(),
removeListener: jest.fn(),
stopAnimation: jest.fn(),
})),
timing: jest.fn(() => ({
start: jest.fn((callback) => callback && callback({ finished: true })),
stop: jest.fn(),
})),
sequence: jest.fn(() => ({
start: jest.fn((callback) => callback && callback({ finished: true })),
stop: jest.fn(),
})),
loop: jest.fn(() => ({
start: jest.fn((callback) => callback && callback({ finished: true })),
stop: jest.fn(),
})),
createAnimatedComponent: jest.fn((component) => component),
},
Alert: { alert: jest.fn() },
Keyboard: { avoidView: 'KeyboardAvoidingView', dismiss: jest.fn() },
FlatList: 'FlatList',
SectionList: 'SectionList',
StatusBar: 'StatusBar',
RefreshControl: 'RefreshControl',
PixelRatio: { get: () => 2 },
I18nManager: { isRTL: false, allowRTL: jest.fn() },
findNodeHandle: jest.fn(),
UIManager: {
measure: jest.fn(),
measureLayout: jest.fn(),
measureInWindow: jest.fn(),
getViewManagerConfig: jest.fn(() => ({})),
},
NativeModules: {
UIManager: { getViewManagerConfig: jest.fn(() => ({})) },
},
AccessibilityInfo: {
isScreenReaderEnabled: jest.fn(() => Promise.resolve(false)),
isReduceMotionEnabled: jest.fn(() => Promise.resolve(false)),
addEventListener: jest.fn((event, handler) => ({ remove: jest.fn() })),
removeEventListener: jest.fn(),
},
InteractionManager: { runAfterInteractions: jest.fn((cb) => cb()) },
}));

// Mock expo-notifications
// Silence console warnings during tests
global.console = {
...console,
warn: jest.fn(),
error: jest.fn(),
};

// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () => ({
setItem: jest.fn(() => Promise.resolve()),
getItem: jest.fn(() => Promise.resolve(null)),
removeItem: jest.fn(() => Promise.resolve()),
clear: jest.fn(() => Promise.resolve()),
getAllKeys: jest.fn(() => Promise.resolve([])),
multiGet: jest.fn(() => Promise.resolve([])),
multiSet: jest.fn(() => Promise.resolve()),
}));

// Mock expo-secure-store to avoid ESM issues
jest.mock('expo-secure-store', () => ({
getItemAsync: jest.fn(() => Promise.resolve(null)),
setItemAsync: jest.fn(() => Promise.resolve()),
deleteItemAsync: jest.fn(() => Promise.resolve()),
}));

// Mock expo-device
jest.mock('expo-device', () => ({
isDevice: true,
deviceName: 'Test Device',
}));

// Mock expo-constants
jest.mock('expo-constants', () => ({
expoConfig: {
extra: {
eas: {
projectId: 'test-project-id',
},
},
},
}));

// Mock expo-linking
jest.mock('expo-linking', () => ({
createURL: jest.fn((path) => `teachlink://${path}`),
addEventListener: jest.fn(() => ({ remove: jest.fn() })),
getInitialURL: jest.fn(() => Promise.resolve(null)),
}));

// Mock expo-notifications (override jest-expo's mock to add removed methods)
jest.mock('expo-notifications', () => ({
setNotificationHandler: jest.fn(),
getPermissionsAsync: jest.fn(() =>
Promise.resolve({ status: 'undetermined' })
),
requestPermissionsAsync: jest.fn(() =>
Promise.resolve({ status: 'granted' })
),
getExpoPushTokenAsync: jest.fn(() =>
Promise.resolve({ data: 'ExponentPushToken[test-token-123]' })
),
getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'undetermined' })),
requestPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
getExpoPushTokenAsync: jest.fn(() => Promise.resolve({ data: 'ExponentPushToken[test-token-123]' })),
setNotificationChannelAsync: jest.fn(() => Promise.resolve()),
scheduleNotificationAsync: jest.fn(() => Promise.resolve('notification-id')),
cancelScheduledNotificationAsync: jest.fn(() => Promise.resolve()),
Expand All @@ -34,17 +154,10 @@ jest.mock('expo-notifications', () => ({
setBadgeCountAsync: jest.fn(() => Promise.resolve()),
addNotificationReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
addNotificationResponseReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
removeNotificationSubscription: jest.fn(),
removeNotificationSubscription: jest.fn(), // deprecated but used in codebase
getLastNotificationResponseAsync: jest.fn(() => Promise.resolve(null)),
AndroidImportance: {
HIGH: 4,
DEFAULT: 3,
},
PermissionStatus: {
GRANTED: 'granted',
DENIED: 'denied',
UNDETERMINED: 'undetermined',
},
AndroidImportance: { HIGH: 4, DEFAULT: 3 },
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
}));

// Mock expo-device
Expand All @@ -71,20 +184,22 @@ jest.mock('expo-linking', () => ({
getInitialURL: jest.fn(() => Promise.resolve(null)),
}));

// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () => ({
setItem: jest.fn(() => Promise.resolve()),
getItem: jest.fn(() => Promise.resolve(null)),
removeItem: jest.fn(() => Promise.resolve()),
clear: jest.fn(() => Promise.resolve()),
getAllKeys: jest.fn(() => Promise.resolve([])),
multiGet: jest.fn(() => Promise.resolve([])),
multiSet: jest.fn(() => Promise.resolve()),
// Mock expo-notifications (override jest-expo's mock to add removed methods)
jest.mock('expo-notifications', () => ({
setNotificationHandler: jest.fn(),
getPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'undetermined' })),
requestPermissionsAsync: jest.fn(() => Promise.resolve({ status: 'granted' })),
getExpoPushTokenAsync: jest.fn(() => Promise.resolve({ data: 'ExponentPushToken[test-token-123]' })),
setNotificationChannelAsync: jest.fn(() => Promise.resolve()),
scheduleNotificationAsync: jest.fn(() => Promise.resolve('notification-id')),
cancelScheduledNotificationAsync: jest.fn(() => Promise.resolve()),
cancelAllScheduledNotificationsAsync: jest.fn(() => Promise.resolve()),
getBadgeCountAsync: jest.fn(() => Promise.resolve(0)),
setBadgeCountAsync: jest.fn(() => Promise.resolve()),
addNotificationReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
addNotificationResponseReceivedListener: jest.fn(() => ({ remove: jest.fn() })),
removeNotificationSubscription: jest.fn(), // deprecated but used in codebase
getLastNotificationResponseAsync: jest.fn(() => Promise.resolve(null)),
AndroidImportance: { HIGH: 4, DEFAULT: 3 },
PermissionStatus: { GRANTED: 'granted', DENIED: 'denied', UNDETERMINED: 'undetermined' },
}));

// Silence console warnings during tests
global.console = {
...console,
warn: jest.fn(),
error: jest.fn(),
};
7 changes: 5 additions & 2 deletions metro.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const { getDefaultConfig } = require('expo/metro-config');
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");

module.exports = getDefaultConfig(__dirname);
const config = getDefaultConfig(__dirname);

module.exports = withNativeWind(config, { input: "./global.css" });
Loading
Loading