Skip to content

Commit 5c19fee

Browse files
authored
Merge pull request #169 from Resgrid/develop
CU-868ffcgaz Fixing unit tests broken by Expo 53 update
2 parents b0908f1 + f54b35e commit 5c19fee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1318
-980
lines changed

.github/workflows/react-native-cicd.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,22 @@ jobs:
147147
yarn install --frozen-lockfile
148148
149149
- name: 📋 Create Google Json File
150+
if: ${{ matrix.platform == 'android' }}
150151
run: |
151152
echo $UNIT_GOOGLE_SERVICES | base64 -d > google-services.json
152153
153154
- name: 📋 Update package.json Versions
154155
run: |
155-
# Check if jq is installed, if not install it
156-
if ! command -v jq &> /dev/null; then
156+
# Ensure jq exists on both Linux and macOS
157+
if ! command -v jq >/dev/null 2>&1; then
157158
echo "Installing jq..."
158-
sudo apt-get update && sudo apt-get install -y jq
159+
if [[ "$RUNNER_OS" == "Linux" ]]; then
160+
sudo apt-get update && sudo apt-get install -y jq
161+
elif [[ "$RUNNER_OS" == "macOS" ]]; then
162+
brew update && brew install jq
163+
else
164+
echo "Unsupported OS for auto-install of jq" >&2; exit 1
165+
fi
159166
fi
160167
161168
# Fix the main entry in package.json

__mocks__/expo-av.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Mock for expo-av
2+
export const Audio = {
3+
setAudioModeAsync: jest.fn().mockResolvedValue(undefined),
4+
Sound: class MockSound {
5+
static createAsync = jest.fn().mockResolvedValue({
6+
sound: new this(),
7+
status: { isLoaded: true },
8+
});
9+
10+
playAsync = jest.fn().mockResolvedValue({ status: { isPlaying: true } });
11+
stopAsync = jest.fn().mockResolvedValue({ status: { isPlaying: false } });
12+
unloadAsync = jest.fn().mockResolvedValue(undefined);
13+
setVolumeAsync = jest.fn().mockResolvedValue(undefined);
14+
},
15+
setIsEnabledAsync: jest.fn().mockResolvedValue(undefined),
16+
getPermissionsAsync: jest.fn().mockResolvedValue({
17+
granted: true,
18+
canAskAgain: true,
19+
expires: 'never',
20+
status: 'granted',
21+
}),
22+
requestPermissionsAsync: jest.fn().mockResolvedValue({
23+
granted: true,
24+
canAskAgain: true,
25+
expires: 'never',
26+
status: 'granted',
27+
}),
28+
};
29+
30+
export const InterruptionModeIOS = {
31+
MixWithOthers: 0,
32+
DoNotMix: 1,
33+
DuckOthers: 2,
34+
};
35+
36+
export const AVPlaybackSource = {};

__mocks__/expo-constants.ts

Whitespace-only changes.

__mocks__/expo-device.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export const isDevice = true;
2+
export const deviceName = 'Test Device';
3+
export const deviceYearClass = 2023;
4+
export const totalMemory = 8192;
5+
export const supportedCpuArchitectures = ['arm64'];
6+
export const osName = 'iOS';
7+
export const osVersion = '15.0';
8+
export const platformApiLevel = null;
9+
export const modelName = 'iPhone 13';
10+
export const modelId = 'iPhone14,5';
11+
export const designName = 'iPhone';
12+
export const productName = 'iPhone';
13+
export const deviceType = 1;
14+
export const manufacturer = 'Apple';
15+
16+
export default {
17+
isDevice,
18+
deviceName,
19+
deviceYearClass,
20+
totalMemory,
21+
supportedCpuArchitectures,
22+
osName,
23+
osVersion,
24+
platformApiLevel,
25+
modelName,
26+
modelId,
27+
designName,
28+
productName,
29+
deviceType,
30+
manufacturer,
31+
};

__mocks__/expo-location.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
export const LocationAccuracy = {
2+
Lowest: 1,
3+
Low: 2,
4+
Balanced: 3,
5+
High: 4,
6+
Highest: 5,
7+
BestForNavigation: 6,
8+
};
9+
10+
export const LocationActivityType = {
11+
Other: 1,
12+
AutomotiveNavigation: 2,
13+
Fitness: 3,
14+
OtherNavigation: 4,
15+
Airborne: 5,
16+
};
17+
18+
export const requestForegroundPermissionsAsync = jest.fn().mockResolvedValue({
19+
status: 'granted',
20+
granted: true,
21+
canAskAgain: true,
22+
expires: 'never',
23+
});
24+
25+
export const requestBackgroundPermissionsAsync = jest.fn().mockResolvedValue({
26+
status: 'granted',
27+
granted: true,
28+
canAskAgain: true,
29+
expires: 'never',
30+
});
31+
32+
export const getCurrentPositionAsync = jest.fn().mockResolvedValue({
33+
coords: {
34+
latitude: 40.7128,
35+
longitude: -74.006,
36+
altitude: null,
37+
accuracy: 5,
38+
altitudeAccuracy: null,
39+
heading: null,
40+
speed: null,
41+
},
42+
timestamp: Date.now(),
43+
});
44+
45+
export const watchPositionAsync = jest.fn().mockImplementation((options, callback) => {
46+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
47+
let hasTimedOut = false;
48+
49+
// Use setTimeout for a one-shot callback to avoid timer leaks
50+
timeoutId = setTimeout(() => {
51+
hasTimedOut = true;
52+
timeoutId = null;
53+
callback({
54+
coords: {
55+
latitude: 40.7128,
56+
longitude: -74.006,
57+
altitude: null,
58+
accuracy: 5,
59+
altitudeAccuracy: null,
60+
heading: 0,
61+
speed: null,
62+
},
63+
timestamp: Date.now(),
64+
});
65+
}, 100); // Shorter delay for faster tests
66+
67+
return Promise.resolve({
68+
remove: () => {
69+
if (timeoutId && !hasTimedOut) {
70+
clearTimeout(timeoutId);
71+
timeoutId = null;
72+
}
73+
// Safe no-op if timeout already fired
74+
},
75+
});
76+
});
77+
78+
export const startLocationUpdatesAsync = jest.fn().mockResolvedValue(undefined);
79+
export const stopLocationUpdatesAsync = jest.fn().mockResolvedValue(undefined);
80+
export const hasStartedLocationUpdatesAsync = jest.fn().mockResolvedValue(false);

__mocks__/expo-navigation-bar.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const setVisibilityAsync = jest.fn().mockResolvedValue(undefined);
2+
export const getVisibilityAsync = jest.fn().mockResolvedValue('visible');
3+
export const setBackgroundColorAsync = jest.fn().mockResolvedValue(undefined);
4+
export const getBackgroundColorAsync = jest.fn().mockResolvedValue('#000000');
5+
export const setBehaviorAsync = jest.fn().mockResolvedValue(undefined);
6+
export const getBehaviorAsync = jest.fn().mockResolvedValue('overlay-swipe');
7+
export const setButtonStyleAsync = jest.fn().mockResolvedValue(undefined);
8+
export const getButtonStyleAsync = jest.fn().mockResolvedValue('light');
9+
export const setPositionAsync = jest.fn().mockResolvedValue(undefined);
10+
export const getPositionAsync = jest.fn().mockResolvedValue('bottom');

__mocks__/expo-task-manager.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const defineTask = jest.fn();
2+
export const startLocationTrackingAsync = jest.fn().mockResolvedValue(undefined);
3+
export const stopLocationTrackingAsync = jest.fn().mockResolvedValue(undefined);
4+
export const hasStartedLocationTrackingAsync = jest.fn().mockResolvedValue(false);
5+
export const getRegisteredTasksAsync = jest.fn().mockResolvedValue([]);
6+
export const isTaskRegisteredAsync = jest.fn().mockResolvedValue(false);
7+
export const unregisterTaskAsync = jest.fn().mockResolvedValue(undefined);
8+
export const unregisterAllTasksAsync = jest.fn().mockResolvedValue(undefined);
9+
10+
const TaskManager = {
11+
defineTask,
12+
startLocationTrackingAsync,
13+
stopLocationTrackingAsync,
14+
hasStartedLocationTrackingAsync,
15+
getRegisteredTasksAsync,
16+
isTaskRegisteredAsync,
17+
unregisterTaskAsync,
18+
unregisterAllTasksAsync,
19+
};
20+
21+
export default TaskManager;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const SystemBars = ({ style, hidden }: any) => null;
2+
3+
export const setStatusBarStyle = jest.fn();
4+
export const setNavigationBarStyle = jest.fn();

jest-platform-setup.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Platform setup for Jest - must run before other modules
2+
const mockPlatform = {
3+
OS: 'ios' as const,
4+
select: jest.fn().mockImplementation((obj: any) => obj.ios || obj.default),
5+
Version: 17,
6+
constants: {},
7+
isTesting: true,
8+
};
9+
10+
// Set global Platform for testing library - must be set before other imports
11+
Object.defineProperty(global, 'Platform', {
12+
value: mockPlatform,
13+
writable: true,
14+
enumerable: true,
15+
configurable: true,
16+
});
17+
18+
// Also mock the react-native Platform module directly
19+
jest.doMock('react-native/Libraries/Utilities/Platform', () => mockPlatform);
20+
21+
// Ensure Platform is available in the global scope for React Navigation and other libs
22+
(global as any).Platform = mockPlatform;

jest-setup.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,48 @@ jest.mock('expo-audio', () => ({
3434
setIsAudioActiveAsync: jest.fn(),
3535
}));
3636

37-
// Mock Platform.OS for React Native
38-
jest.mock('react-native/Libraries/Utilities/Platform', () => ({
39-
OS: 'ios',
40-
select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
41-
}));
37+
// Mock the host component names function to prevent testing library errors
38+
// Check if the internal module exists (for pre-v13 compatibility)
39+
try {
40+
require.resolve('@testing-library/react-native/build/helpers/host-component-names');
41+
// If the internal module exists, mock it (pre-v13)
42+
jest.mock('@testing-library/react-native/build/helpers/host-component-names', () => ({
43+
getHostComponentNames: jest.fn(() => ({
44+
text: 'Text',
45+
view: 'View',
46+
scrollView: 'ScrollView',
47+
touchable: 'TouchableOpacity',
48+
switch: 'Switch',
49+
textInput: 'TextInput',
50+
})),
51+
configureHostComponentNamesIfNeeded: jest.fn(),
52+
isHostText: jest.fn((element) => element?.type === 'Text' || element?._fiber?.type === 'Text' || (typeof element === 'object' && element?.props?.children && typeof element.props.children === 'string')),
53+
isHostTextInput: jest.fn((element) => element?.type === 'TextInput' || element?._fiber?.type === 'TextInput'),
54+
isHostImage: jest.fn((element) => element?.type === 'Image' || element?._fiber?.type === 'Image'),
55+
isHostSwitch: jest.fn((element) => element?.type === 'Switch' || element?._fiber?.type === 'Switch'),
56+
isHostScrollView: jest.fn((element) => element?.type === 'ScrollView' || element?._fiber?.type === 'ScrollView'),
57+
isHostModal: jest.fn((element) => element?.type === 'Modal' || element?._fiber?.type === 'Modal'),
58+
}));
59+
} catch (error) {
60+
// Module doesn't exist (v13+), try to use the public API if available
61+
try {
62+
const { configureHostComponentNames } = require('@testing-library/react-native');
63+
// Configure host component names using the public API (v13+)
64+
if (configureHostComponentNames) {
65+
configureHostComponentNames({
66+
text: 'Text',
67+
view: 'View',
68+
scrollView: 'ScrollView',
69+
touchable: 'TouchableOpacity',
70+
switch: 'Switch',
71+
textInput: 'TextInput',
72+
});
73+
}
74+
} catch (publicApiError) {
75+
// If neither internal nor public API is available, log a warning but continue
76+
console.warn('Unable to configure host component names for @testing-library/react-native. Tests may fail if they rely on component type detection.');
77+
}
78+
}
4279

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

0 commit comments

Comments
 (0)