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
13 changes: 10 additions & 3 deletions .github/workflows/react-native-cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,22 @@ jobs:
yarn install --frozen-lockfile

- name: 📋 Create Google Json File
if: ${{ matrix.platform == 'android' }}
run: |
echo $UNIT_GOOGLE_SERVICES | base64 -d > google-services.json

- name: 📋 Update package.json Versions
run: |
# Check if jq is installed, if not install it
if ! command -v jq &> /dev/null; then
# Ensure jq exists on both Linux and macOS
if ! command -v jq >/dev/null 2>&1; then
echo "Installing jq..."
sudo apt-get update && sudo apt-get install -y jq
if [[ "$RUNNER_OS" == "Linux" ]]; then
sudo apt-get update && sudo apt-get install -y jq
elif [[ "$RUNNER_OS" == "macOS" ]]; then
brew update && brew install jq
else
echo "Unsupported OS for auto-install of jq" >&2; exit 1
fi
fi

# Fix the main entry in package.json
Expand Down
36 changes: 36 additions & 0 deletions __mocks__/expo-av.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Mock for expo-av
export const Audio = {
setAudioModeAsync: jest.fn().mockResolvedValue(undefined),
Sound: class MockSound {
static createAsync = jest.fn().mockResolvedValue({
sound: new this(),
status: { isLoaded: true },
});

playAsync = jest.fn().mockResolvedValue({ status: { isPlaying: true } });
stopAsync = jest.fn().mockResolvedValue({ status: { isPlaying: false } });
unloadAsync = jest.fn().mockResolvedValue(undefined);
setVolumeAsync = jest.fn().mockResolvedValue(undefined);
},
setIsEnabledAsync: jest.fn().mockResolvedValue(undefined),
getPermissionsAsync: jest.fn().mockResolvedValue({
granted: true,
canAskAgain: true,
expires: 'never',
status: 'granted',
}),
requestPermissionsAsync: jest.fn().mockResolvedValue({
granted: true,
canAskAgain: true,
expires: 'never',
status: 'granted',
}),
};

export const InterruptionModeIOS = {
MixWithOthers: 0,
DoNotMix: 1,
DuckOthers: 2,
};

export const AVPlaybackSource = {};
Empty file added __mocks__/expo-constants.ts
Empty file.
31 changes: 31 additions & 0 deletions __mocks__/expo-device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const isDevice = true;
export const deviceName = 'Test Device';
export const deviceYearClass = 2023;
export const totalMemory = 8192;
export const supportedCpuArchitectures = ['arm64'];
export const osName = 'iOS';
export const osVersion = '15.0';
export const platformApiLevel = null;
export const modelName = 'iPhone 13';
export const modelId = 'iPhone14,5';
export const designName = 'iPhone';
export const productName = 'iPhone';
export const deviceType = 1;
export const manufacturer = 'Apple';

export default {
isDevice,
deviceName,
deviceYearClass,
totalMemory,
supportedCpuArchitectures,
osName,
osVersion,
platformApiLevel,
modelName,
modelId,
designName,
productName,
deviceType,
manufacturer,
};
80 changes: 80 additions & 0 deletions __mocks__/expo-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
export const LocationAccuracy = {
Lowest: 1,
Low: 2,
Balanced: 3,
High: 4,
Highest: 5,
BestForNavigation: 6,
};

export const LocationActivityType = {
Other: 1,
AutomotiveNavigation: 2,
Fitness: 3,
OtherNavigation: 4,
Airborne: 5,
};

export const requestForegroundPermissionsAsync = jest.fn().mockResolvedValue({
status: 'granted',
granted: true,
canAskAgain: true,
expires: 'never',
});

export const requestBackgroundPermissionsAsync = jest.fn().mockResolvedValue({
status: 'granted',
granted: true,
canAskAgain: true,
expires: 'never',
});

export const getCurrentPositionAsync = jest.fn().mockResolvedValue({
coords: {
latitude: 40.7128,
longitude: -74.006,
altitude: null,
accuracy: 5,
altitudeAccuracy: null,
heading: null,
speed: null,
},
timestamp: Date.now(),
});

export const watchPositionAsync = jest.fn().mockImplementation((options, callback) => {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let hasTimedOut = false;

// Use setTimeout for a one-shot callback to avoid timer leaks
timeoutId = setTimeout(() => {
hasTimedOut = true;
timeoutId = null;
callback({
coords: {
latitude: 40.7128,
longitude: -74.006,
altitude: null,
accuracy: 5,
altitudeAccuracy: null,
heading: 0,
speed: null,
},
timestamp: Date.now(),
});
}, 100); // Shorter delay for faster tests

return Promise.resolve({
remove: () => {
if (timeoutId && !hasTimedOut) {
clearTimeout(timeoutId);
timeoutId = null;
}
// Safe no-op if timeout already fired
},
});
});

export const startLocationUpdatesAsync = jest.fn().mockResolvedValue(undefined);
export const stopLocationUpdatesAsync = jest.fn().mockResolvedValue(undefined);
export const hasStartedLocationUpdatesAsync = jest.fn().mockResolvedValue(false);
10 changes: 10 additions & 0 deletions __mocks__/expo-navigation-bar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const setVisibilityAsync = jest.fn().mockResolvedValue(undefined);
export const getVisibilityAsync = jest.fn().mockResolvedValue('visible');
export const setBackgroundColorAsync = jest.fn().mockResolvedValue(undefined);
export const getBackgroundColorAsync = jest.fn().mockResolvedValue('#000000');
export const setBehaviorAsync = jest.fn().mockResolvedValue(undefined);
export const getBehaviorAsync = jest.fn().mockResolvedValue('overlay-swipe');
export const setButtonStyleAsync = jest.fn().mockResolvedValue(undefined);
export const getButtonStyleAsync = jest.fn().mockResolvedValue('light');
export const setPositionAsync = jest.fn().mockResolvedValue(undefined);
export const getPositionAsync = jest.fn().mockResolvedValue('bottom');
21 changes: 21 additions & 0 deletions __mocks__/expo-task-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const defineTask = jest.fn();
export const startLocationTrackingAsync = jest.fn().mockResolvedValue(undefined);
export const stopLocationTrackingAsync = jest.fn().mockResolvedValue(undefined);
export const hasStartedLocationTrackingAsync = jest.fn().mockResolvedValue(false);
export const getRegisteredTasksAsync = jest.fn().mockResolvedValue([]);
export const isTaskRegisteredAsync = jest.fn().mockResolvedValue(false);
export const unregisterTaskAsync = jest.fn().mockResolvedValue(undefined);
export const unregisterAllTasksAsync = jest.fn().mockResolvedValue(undefined);

const TaskManager = {
defineTask,
startLocationTrackingAsync,
stopLocationTrackingAsync,
hasStartedLocationTrackingAsync,
getRegisteredTasksAsync,
isTaskRegisteredAsync,
unregisterTaskAsync,
unregisterAllTasksAsync,
};

export default TaskManager;
4 changes: 4 additions & 0 deletions __mocks__/react-native-edge-to-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const SystemBars = ({ style, hidden }: any) => null;

export const setStatusBarStyle = jest.fn();
export const setNavigationBarStyle = jest.fn();
22 changes: 22 additions & 0 deletions jest-platform-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Platform setup for Jest - must run before other modules
const mockPlatform = {
OS: 'ios' as const,
select: jest.fn().mockImplementation((obj: any) => obj.ios || obj.default),
Version: 17,
constants: {},
isTesting: true,
};

// Set global Platform for testing library - must be set before other imports
Object.defineProperty(global, 'Platform', {
value: mockPlatform,
writable: true,
enumerable: true,
configurable: true,
});

// Also mock the react-native Platform module directly
jest.doMock('react-native/Libraries/Utilities/Platform', () => mockPlatform);

// Ensure Platform is available in the global scope for React Navigation and other libs
(global as any).Platform = mockPlatform;

Check warning on line 22 in jest-platform-setup.ts

View workflow job for this annotation

GitHub Actions / test

Insert `⏎`
47 changes: 42 additions & 5 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,48 @@ jest.mock('expo-audio', () => ({
setIsAudioActiveAsync: jest.fn(),
}));

// Mock Platform.OS for React Native
jest.mock('react-native/Libraries/Utilities/Platform', () => ({
OS: 'ios',
select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
}));
// Mock the host component names function to prevent testing library errors
// Check if the internal module exists (for pre-v13 compatibility)
try {
require.resolve('@testing-library/react-native/build/helpers/host-component-names');
// If the internal module exists, mock it (pre-v13)
jest.mock('@testing-library/react-native/build/helpers/host-component-names', () => ({
getHostComponentNames: jest.fn(() => ({
text: 'Text',
view: 'View',
scrollView: 'ScrollView',
touchable: 'TouchableOpacity',
switch: 'Switch',
textInput: 'TextInput',
})),
configureHostComponentNamesIfNeeded: jest.fn(),
isHostText: jest.fn((element) => element?.type === 'Text' || element?._fiber?.type === 'Text' || (typeof element === 'object' && element?.props?.children && typeof element.props.children === 'string')),
isHostTextInput: jest.fn((element) => element?.type === 'TextInput' || element?._fiber?.type === 'TextInput'),
isHostImage: jest.fn((element) => element?.type === 'Image' || element?._fiber?.type === 'Image'),
isHostSwitch: jest.fn((element) => element?.type === 'Switch' || element?._fiber?.type === 'Switch'),
isHostScrollView: jest.fn((element) => element?.type === 'ScrollView' || element?._fiber?.type === 'ScrollView'),
isHostModal: jest.fn((element) => element?.type === 'Modal' || element?._fiber?.type === 'Modal'),
}));
} catch (error) {
// Module doesn't exist (v13+), try to use the public API if available
try {
const { configureHostComponentNames } = require('@testing-library/react-native');
// Configure host component names using the public API (v13+)
if (configureHostComponentNames) {
configureHostComponentNames({
text: 'Text',
view: 'View',
scrollView: 'ScrollView',
touchable: 'TouchableOpacity',
switch: 'Switch',
textInput: 'TextInput',
});
}
} catch (publicApiError) {
// If neither internal nor public API is available, log a warning but continue
console.warn('Unable to configure host component names for @testing-library/react-native. Tests may fail if they rely on component type detection.');
}
}

// Global mocks for common problematic modules
jest.mock('@notifee/react-native', () => {
Expand Down
Loading
Loading