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
11 changes: 11 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ global.window = {};
// @ts-ignore
global.window = global;

// Enable Jest fake timers globally for proper timer handling
jest.useFakeTimers();

// Polyfill setImmediate if needed (for React Native environment)
if (typeof global.setImmediate === 'undefined') {
// @ts-ignore - Simple polyfill for setImmediate
global.setImmediate = (callback: (...args: any[]) => void, ...args: any[]) => {
return setTimeout(callback, 0, ...args);
};
}

// Mock React Native Appearance for NativeWind
jest.mock('react-native/Libraries/Utilities/Appearance', () => ({
getColorScheme: jest.fn(() => 'light'),
Expand Down
23 changes: 15 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"@notifee/react-native": "^9.1.8",
"@novu/react-native": "~2.6.6",
"@react-native-community/netinfo": "11.4.1",
"@rnmapbox/maps": "10.1.38",
"@rnmapbox/maps": "10.1.42-rc.0",
"@semantic-release/git": "^10.0.1",
"@sentry/react-native": "~6.10.0",
"@shopify/flash-list": "1.7.3",
Expand Down Expand Up @@ -137,26 +137,27 @@
"react-error-boundary": "~4.0.13",
"react-hook-form": "~7.53.0",
"react-i18next": "~15.0.1",
"react-native": "0.76.9",
"react-native": "0.77.3",
"react-native-base64": "~0.2.1",
"react-native-ble-manager": "^12.1.5",
"react-native-calendars": "^1.1313.0",
"react-native-callkeep": "github:Irfanwani/react-native-callkeep#957193d0716f1c2dfdc18e627cbff0f8a0800971",
"react-native-edge-to-edge": "~1.1.2",
"react-native-flash-message": "~0.4.2",
"react-native-gesture-handler": "~2.20.2",
"react-native-gesture-handler": "~2.22.0",
"react-native-get-random-values": "^1.11.0",
"react-native-keyboard-controller": "~1.15.2",
"react-native-logs": "~5.3.0",
"react-native-mmkv": "~3.1.0",
"react-native-permissions": "^5.4.1",
"react-native-reanimated": "~3.16.1",
"react-native-reanimated": "~3.16.7",
"react-native-restart": "0.0.27",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-safe-area-context": "~5.1.0",
"react-native-screens": "~4.8.0",
"react-native-svg": "~15.8.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"react-native-webview": "~13.13.1",
"react-query-kit": "~3.3.0",
"sanitize-html": "^2.17.0",
"tailwind-variants": "~0.2.1",
Expand Down Expand Up @@ -226,7 +227,13 @@
},
"install": {
"exclude": [
"eslint-config-expo"
"eslint-config-expo",
"react-native@~0.76.6",
"react-native-reanimated@~3.16.1",
"react-native-gesture-handler@~2.20.0",
"react-native-screens@~4.4.0",
"react-native-safe-area-context@~4.12.0",
"react-native-webview@~13.12.5"
]
}
},
Expand Down
8 changes: 4 additions & 4 deletions src/api/common/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export const api = axiosInstance;
// Helper function to create API endpoints
export const createApiEndpoint = (endpoint: string) => {
return {
get: <T,>(params?: Record<string, unknown>) => api.get<T>(endpoint, { params }),
post: <T,>(data: Record<string, unknown>) => api.post<T>(endpoint, data),
put: <T,>(data: Record<string, unknown>) => api.put<T>(endpoint, data),
delete: <T,>(params?: Record<string, unknown>) => api.delete<T>(endpoint, { params }),
get: <T,>(params?: Record<string, unknown>, signal?: AbortSignal) => api.get<T>(endpoint, { params, signal }),
post: <T,>(data: Record<string, unknown>, signal?: AbortSignal) => api.post<T>(endpoint, data, { signal }),
put: <T,>(data: Record<string, unknown>, signal?: AbortSignal) => api.put<T>(endpoint, data, { signal }),
delete: <T,>(params?: Record<string, unknown>, signal?: AbortSignal) => api.delete<T>(endpoint, { params, signal }),
};
};
6 changes: 3 additions & 3 deletions src/api/mapping/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { type GetMapLayersResult } from '@/models/v4/mapping/getMapLayersResult'

import { createApiEndpoint } from '../common/client';

const getMayLayersApi = createApiEndpoint('/Mapping/GetMayLayers');
const getMayLayersApi = createApiEndpoint('/Mapping/GetMapLayers');

const getMapDataAndMarkersApi = createApiEndpoint('/Mapping/GetMapDataAndMarkers');

export const getMapDataAndMarkers = async () => {
const response = await getMapDataAndMarkersApi.get<GetMapDataAndMarkersResult>();
export const getMapDataAndMarkers = async (signal?: AbortSignal) => {
const response = await getMapDataAndMarkersApi.get<GetMapDataAndMarkersResult>(undefined, signal);
return response.data;
};

Expand Down
2 changes: 2 additions & 0 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Import URL polyfill at the very top to ensure URL is available globally
import 'react-native-url-polyfill/auto';
// Import global CSS file
import '../../global.css';
import '../lib/i18n';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,25 @@ jest.mock('nativewind', () => ({
useColorScheme: () => ({ colorScheme: 'light' }),
}));

// Mock React Native APIs with isolated mocking
jest.mock('react-native/Libraries/Settings/Settings.ios', () => ({}));
jest.mock('react-native/Libraries/Settings/NativeSettingsManager', () => ({
getConstants: () => ({}),
get: jest.fn(),
set: jest.fn(),
}));

// Partial mock of React Native - preserve all original exports and only override Platform.OS
jest.mock('react-native', () => ({
...jest.requireActual('react-native'),
Platform: {
...jest.requireActual('react-native').Platform,
OS: 'ios',
select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
},
ScrollView: ({ children, ...props }: any) => {
const React = require('react');
return React.createElement('View', { testID: 'scroll-view', ...props }, children);
},
useWindowDimensions: () => ({
width: 400,
height: 800,
}),
}));
Comment on lines 15 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Over-mocking 'react-native' drops core components; use requireActual and override only what you need

This mock removes View/Text/TextInput exports and can break host components and library internals.

-jest.mock('react-native', () => ({
-  Platform: {
-    OS: 'ios',
-    select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
-  },
-  ScrollView: ({ children, ...props }: any) => {
-    const React = require('react');
-    return React.createElement('View', { testID: 'scroll-view', ...props }, children);
-  },
-  useWindowDimensions: () => ({
-    width: 400,
-    height: 800,
-  }),
-}));
+jest.mock('react-native', () => {
+  const RN = jest.requireActual('react-native');
+  const React = require('react');
+  return {
+    ...RN,
+    Platform: {
+      ...RN.Platform,
+      OS: 'ios',
+      select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
+    },
+    ScrollView: ({ children, ...props }: any) =>
+      React.createElement(RN.View, { testID: 'scroll-view', ...props }, children),
+    useWindowDimensions: () => ({ width: 400, height: 800 }),
+  };
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock('react-native', () => ({
...jest.requireActual('react-native'),
Platform: {
...jest.requireActual('react-native').Platform,
OS: 'ios',
select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
},
ScrollView: ({ children, ...props }: any) => {
const React = require('react');
return React.createElement('View', { testID: 'scroll-view', ...props }, children);
},
useWindowDimensions: () => ({
width: 400,
height: 800,
}),
}));
// src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
jest.mock('react-native', () => {
const RN = jest.requireActual('react-native');
const React = require('react');
return {
...RN,
Platform: {
...RN.Platform,
OS: 'ios',
select: jest.fn().mockImplementation((obj) => obj.ios || obj.default),
},
ScrollView: ({ children, ...props }: any) =>
React.createElement(RN.View, { testID: 'scroll-view', ...props }, children),
useWindowDimensions: () => ({ width: 400, height: 800 }),
};
});
🤖 Prompt for AI Agents
In src/components/settings/__tests__/server-url-bottom-sheet-simple.test.tsx
around lines 15 to 28, the test currently mocks the entire 'react-native' module
which removes core exports (View, Text, TextInput, etc.) and breaks components;
change the mock to call requireActual('react-native') and spread its exports,
then override only Platform (with OS and select), useWindowDimensions, and if
necessary ScrollView — this preserves core components while replacing just the
bits you need for the test.


jest.mock('@/hooks/use-analytics', () => ({
useAnalytics: () => ({
trackEvent: jest.fn(),
}),
}));

jest.mock('react-hook-form', () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
// Reset the store to initial state
useLiveKitCallStore.setState({
availableRooms: [
{ id: 'emergency-channel', name: 'Emergency Channel' },
{ id: 'tactical-1', name: 'Tactical 1' },
{ id: 'dispatch', name: 'Dispatch' },
{ id: 'general-chat', name: 'General Chat' },
{ id: 'dev-team-sync', name: 'Dev Team Sync' },
{ id: 'product-updates', name: 'Product Updates' },
],
selectedRoomForJoining: null,
currentRoomId: null,
Expand Down Expand Up @@ -155,11 +155,11 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {

describe('Room Connection with CallKeep', () => {
beforeEach(() => {
// Mock successful connection flow
// Mock successful connection flow with proper async handling
mockRoom.on.mockImplementation((event: any, callback: any) => {
if (event === 'connectionStateChanged') {
// Simulate connected state
setTimeout(() => callback('connected'), 0);
// Store the callback for manual triggering
(mockRoom as any)._connectionStateCallback = callback;
}
return mockRoom;
});
Expand All @@ -169,20 +169,28 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
const { result } = renderHook(() => useLiveKitCallStore());

await act(async () => {
await result.current.actions.connectToRoom('emergency-channel', 'test-participant');
// Wait for the connection state change event to fire
await new Promise(resolve => setTimeout(resolve, 100));
await result.current.actions.connectToRoom('general-chat', 'test-participant');

// Manually trigger the connection state change
if ((mockRoom as any)._connectionStateCallback) {
(mockRoom as any)._connectionStateCallback('connected');
}
});

expect(mockCallKeepService.startCall).toHaveBeenCalledWith('emergency-channel');
expect(mockCallKeepService.startCall).toHaveBeenCalledWith('general-chat');
});

it('should not start CallKeep call on Android', async () => {
mockPlatform.OS = 'android';
const { result } = renderHook(() => useLiveKitCallStore());

await act(async () => {
await result.current.actions.connectToRoom('emergency-channel', 'test-participant');
await result.current.actions.connectToRoom('dev-team-sync', 'test-participant');

// Manually trigger the connection state change
if ((mockRoom as any)._connectionStateCallback) {
(mockRoom as any)._connectionStateCallback('connected');
}
});

expect(mockCallKeepService.startCall).not.toHaveBeenCalled();
Expand All @@ -195,14 +203,17 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
const { result } = renderHook(() => useLiveKitCallStore());

await act(async () => {
await result.current.actions.connectToRoom('emergency-channel', 'test-participant');
// Wait for the connection state change event to fire
await new Promise(resolve => setTimeout(resolve, 100));
await result.current.actions.connectToRoom('general-chat', 'test-participant');

// Manually trigger the connection state change
if ((mockRoom as any)._connectionStateCallback) {
(mockRoom as any)._connectionStateCallback('connected');
}
});

expect(mockLogger.warn).toHaveBeenCalledWith({
message: 'Failed to start CallKeep call (background audio may not work)',
context: { error, roomId: 'emergency-channel' },
context: { error, roomId: 'general-chat' },
});
});
});
Expand Down Expand Up @@ -345,6 +356,7 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
const { result } = renderHook(() => useLiveKitCallStore());

expect(result.current.availableRooms).toHaveLength(3);
expect(result.current.availableRooms[0]).toEqual({ id: 'general-chat', name: 'General Chat' });
expect(result.current.selectedRoomForJoining).toBeNull();
expect(result.current.currentRoomId).toBeNull();
expect(result.current.isConnecting).toBe(false);
Expand Down Expand Up @@ -430,6 +442,8 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
message: 'Error setting microphone state',
context: { error, enabled: true },
});

// Check that error state was set after the await completes
expect(result.current.error).toBe('Could not change microphone state.');
});

Expand Down Expand Up @@ -500,7 +514,6 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
});

describe('Error Handling', () => {
describe('Error Handling', () => {
it('should handle room initialization errors', async () => {
// Make the Room constructor throw an error
MockedRoom.mockImplementationOnce(() => {
Expand Down Expand Up @@ -534,17 +547,4 @@ describe('useLiveKitCallStore with CallKeep Integration', () => {
expect(result.current.error).toBeNull();
});
});

it('should handle basic error state management', async () => {
const { result } = renderHook(() => useLiveKitCallStore());

// Test basic error clearing functionality since token fetching isn't implemented
act(() => {
// Set an error state and then clear it
result.current.actions._clearError();
});

expect(result.current.error).toBeNull();
});
});
});
2 changes: 1 addition & 1 deletion src/features/livekit-call/store/useLiveKitCallStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ConnectionState, type LocalParticipant, type Participant, type RemoteParticipant, Room, type RoomConnectOptions, RoomEvent, type RoomOptions } from 'livekit-client'; // livekit-react-native re-exports these
import { Platform } from 'react-native';
import create from 'zustand';
import { create } from 'zustand';

import { logger } from '../../../lib/logging';
import { callKeepService } from '../../../services/callkeep.service.ios';
Expand Down
Loading
Loading