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
12 changes: 12 additions & 0 deletions __mocks__/@livekit/react-native-webrtc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Mock for @livekit/react-native-webrtc
export const RTCAudioSession = {
configure: jest.fn().mockResolvedValue(undefined),
setCategory: jest.fn().mockResolvedValue(undefined),
setMode: jest.fn().mockResolvedValue(undefined),
getActiveAudioSession: jest.fn().mockReturnValue(null),
setActive: jest.fn().mockResolvedValue(undefined),
};

export default {
RTCAudioSession,
};
2 changes: 1 addition & 1 deletion expo-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/// <reference types="expo/types" />

// NOTE: This file should not be edited and should be in your git ignore
// NOTE: This file should not be edited and should be in your git ignore
114 changes: 61 additions & 53 deletions src/api/calls/calls.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { cacheManager } from '@/lib/cache/cache-manager';
import { type ActiveCallsResult } from '@/models/v4/calls/activeCallsResult';
import { type CallExtraDataResult } from '@/models/v4/calls/callExtraDataResult';
import { type CallResult } from '@/models/v4/calls/callResult';
import { type SaveCallResult } from '@/models/v4/calls/saveCallResult';

import { createCachedApiEndpoint } from '../common/cached-client';
import { createApiEndpoint } from '../common/client';

const callsApi = createApiEndpoint('/Calls/GetActiveCalls');
const callsApi = createCachedApiEndpoint('/Calls/GetActiveCalls', {
ttl: 30 * 1000, // Cache for 30 seconds - calls can change frequently
enabled: true,
});
const getCallApi = createApiEndpoint('/Calls/GetCall');
const getCallExtraDataApi = createApiEndpoint('/Calls/GetCallExtraData');
const createCallApi = createApiEndpoint('/Calls/SaveCall');
Expand Down Expand Up @@ -78,33 +83,34 @@ export interface CloseCallRequest {
note?: string;
}

export const createCall = async (callData: CreateCallRequest) => {
let dispatchList = '';

if (callData.dispatchEveryone) {
dispatchList = '0';
} else {
const dispatchEntries: string[] = [];

if (callData.dispatchUsers) {
//dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
dispatchEntries.push(...callData.dispatchUsers);
}
if (callData.dispatchGroups) {
//dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
dispatchEntries.push(...callData.dispatchGroups);
}
if (callData.dispatchRoles) {
//dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
dispatchEntries.push(...callData.dispatchRoles);
}
if (callData.dispatchUnits) {
//dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
dispatchEntries.push(...callData.dispatchUnits);
}

dispatchList = dispatchEntries.join('|');
/**
* Helper function to build the dispatch list string from dispatch data
*/
const buildDispatchList = (data: { dispatchEveryone?: boolean; dispatchUsers?: string[]; dispatchGroups?: string[]; dispatchRoles?: string[]; dispatchUnits?: string[] }): string => {
if (data.dispatchEveryone) {
return '0';
}

const dispatchEntries: string[] = [];

if (data.dispatchUsers) {
dispatchEntries.push(...data.dispatchUsers);
}
if (data.dispatchGroups) {
dispatchEntries.push(...data.dispatchGroups);
}
if (data.dispatchRoles) {
dispatchEntries.push(...data.dispatchRoles);
}
if (data.dispatchUnits) {
dispatchEntries.push(...data.dispatchUnits);
}

return dispatchEntries.join('|');
};

export const createCall = async (callData: CreateCallRequest) => {
const dispatchList = buildDispatchList(callData);

const data = {
Name: callData.name,
Expand All @@ -122,36 +128,20 @@ export const createCall = async (callData: CreateCallRequest) => {
};

const response = await createCallApi.post<SaveCallResult>(data);

// Invalidate cache after successful mutation
try {
cacheManager.remove('/Calls/GetActiveCalls');
} catch (error) {
// Silently handle cache removal errors
console.warn('Failed to invalidate calls cache:', error);
}

return response.data;
};

export const updateCall = async (callData: UpdateCallRequest) => {
let dispatchList = '';

if (callData.dispatchEveryone) {
dispatchList = '0';
} else {
const dispatchEntries: string[] = [];

if (callData.dispatchUsers) {
//dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
dispatchEntries.push(...callData.dispatchUsers);
}
if (callData.dispatchGroups) {
//dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
dispatchEntries.push(...callData.dispatchGroups);
}
if (callData.dispatchRoles) {
//dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
dispatchEntries.push(...callData.dispatchRoles);
}
if (callData.dispatchUnits) {
//dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
dispatchEntries.push(...callData.dispatchUnits);
}

dispatchList = dispatchEntries.join('|');
}
const dispatchList = buildDispatchList(callData);

const data = {
CallId: callData.callId,
Expand All @@ -170,6 +160,15 @@ export const updateCall = async (callData: UpdateCallRequest) => {
};

const response = await updateCallApi.post<SaveCallResult>(data);

// Invalidate cache after successful mutation
try {
cacheManager.remove('/Calls/GetActiveCalls');
} catch (error) {
// Silently handle cache removal errors
console.warn('Failed to invalidate calls cache:', error);
}

return response.data;
};

Expand All @@ -181,5 +180,14 @@ export const closeCall = async (callData: CloseCallRequest) => {
};

const response = await closeCallApi.put<SaveCallResult>(data);

// Invalidate cache after successful mutation
try {
cacheManager.remove('/Calls/GetActiveCalls');
} catch (error) {
// Silently handle cache removal errors
console.warn('Failed to invalidate calls cache:', error);
}

return response.data;
};
83 changes: 35 additions & 48 deletions src/components/calls/__tests__/call-files-modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,29 @@ const defaultMockFiles = [
},
];

let mockStoreState: any = {
callFiles: defaultMockFiles,
// Create a single object that will be mutated - never reassign!
const mockStoreState = {
callFiles: defaultMockFiles as any,
isLoadingFiles: false,
errorFiles: null,
errorFiles: null as string | null,
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};

// Helper function to update mock state without replacing the object
const setMockStoreState = (updates: Partial<typeof mockStoreState>) => {
Object.assign(mockStoreState, updates);
};

// Reset mock state to defaults
const resetMockStoreState = () => {
mockStoreState.callFiles = defaultMockFiles;
mockStoreState.isLoadingFiles = false;
mockStoreState.errorFiles = null;
mockStoreState.fetchCallFiles = mockFetchCallFiles;
mockStoreState.clearFiles = mockClearFiles;
};

Comment on lines +40 to +62
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant lines
head -80 src/components/calls/__tests__/call-files-modal.test.tsx | tail -50

Repository: Resgrid/Unit

Length of output: 1409


🏁 Script executed:

# Search for existing CallFile or related types in non-test files
rg -n "type CallFile|interface CallFile|type.*CallFile|export.*CallFile" -g '!**/__tests__/**'

Repository: Resgrid/Unit

Length of output: 1866


🏁 Script executed:

# Also check for any models or types related to call files
rg -n "class CallFile|type Call.*File|interface Call.*File" --type ts --type tsx -g '!**/__tests__/**' | head -20

Repository: Resgrid/Unit

Length of output: 83


🏁 Script executed:

# Let's check the CallFileResultData structure to confirm it matches the mock
head -50 src/models/v4/callFiles/callFileResultData.ts

Repository: Resgrid/Unit

Length of output: 407


🏁 Script executed:

# Check if callFiles array is mutated anywhere in the component
rg -A 5 -B 5 "callFiles\." src/components/calls/call-files-modal.tsx | head -40

Repository: Resgrid/Unit

Length of output: 1031


Replace as any with CallFileResultData[] type.

Line 42 uses as any, which conflicts with the TS guideline to avoid any and strive for precise types. Since CallFileResultData is already imported in the component and matches the mock object structure exactly, use it directly instead of the type cast.

Cloning defaultMockFiles in the initial state and reset function is optional but recommended for test isolation if mock state handling evolves.

🔧 Suggested fix
const mockStoreState = {
-  callFiles: defaultMockFiles as any,
+  callFiles: defaultMockFiles as CallFileResultData[],
   isLoadingFiles: false,
   errorFiles: null as string | null,
   fetchCallFiles: mockFetchCallFiles,
   clearFiles: mockClearFiles,
 };
🤖 Prompt for AI Agents
In `@src/components/calls/__tests__/call-files-modal.test.tsx` around lines 40 -
62, The mock store's callFiles currently uses an unsafe cast ("as any"); change
its type to CallFileResultData[] for precision by declaring callFiles:
defaultMockFiles as CallFileResultData[] (or initialize with a shallow clone
like [...defaultMockFiles] to avoid shared mutation), and update the
resetMockStoreState assignment to reset mockStoreState.callFiles =
defaultMockFiles as CallFileResultData[] (or a clone) so the types align; keep
the helpers setMockStoreState, mockFetchCallFiles, and mockClearFiles unchanged.

jest.mock('@/stores/calls/detail-store', () => ({
useCallDetailStore: () => mockStoreState,
}));
Expand Down Expand Up @@ -301,13 +316,7 @@ describe('CallFilesModal', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset to default state
mockStoreState = {
callFiles: defaultMockFiles,
isLoadingFiles: false,
errorFiles: null,
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};
resetMockStoreState();
});

it('renders correctly when closed', () => {
Expand Down Expand Up @@ -421,13 +430,11 @@ describe('CallFilesModal', () => {
describe('Loading States', () => {
beforeEach(() => {
// Mock loading state
mockStoreState = {
setMockStoreState({
callFiles: null,
isLoadingFiles: true,
errorFiles: null,
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};
});
});

it('displays loading spinner when fetching files', () => {
Expand All @@ -443,13 +450,11 @@ describe('CallFilesModal', () => {
describe('Error States', () => {
beforeEach(() => {
// Mock error state
mockStoreState = {
setMockStoreState({
callFiles: [],
isLoadingFiles: false,
errorFiles: 'Network error occurred',
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};
});
});

it('displays error message when file fetch fails', () => {
Expand All @@ -476,13 +481,11 @@ describe('CallFilesModal', () => {
describe('Empty States', () => {
beforeEach(() => {
// Mock empty state
mockStoreState = {
setMockStoreState({
callFiles: [],
isLoadingFiles: false,
errorFiles: null,
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};
});
});

it('displays empty state when no files available', () => {
Expand All @@ -503,12 +506,11 @@ describe('CallFilesModal', () => {

beforeEach(() => {
// Reset to default state with files
mockStoreState = {
setMockStoreState({
callFiles: defaultMockFiles,
isLoadingFiles: false,
errorFiles: null,
fetchCallFiles: mockFetchCallFiles,
};
});
});

it('downloads and shares file when clicked', async () => {
Expand Down Expand Up @@ -591,13 +593,7 @@ describe('CallFilesModal', () => {
describe('File Format Utilities', () => {
beforeEach(() => {
// Reset to default state
mockStoreState = {
callFiles: defaultMockFiles,
isLoadingFiles: false,
errorFiles: null,
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};
resetMockStoreState();
});

it('formats file sizes correctly', () => {
Expand Down Expand Up @@ -625,13 +621,7 @@ describe('CallFilesModal', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset to default state
mockStoreState = {
callFiles: defaultMockFiles,
isLoadingFiles: false,
errorFiles: null,
fetchCallFiles: mockFetchCallFiles,
clearFiles: mockClearFiles,
};
resetMockStoreState();
});

it('should track analytics event when modal is opened', () => {
Expand All @@ -653,10 +643,9 @@ describe('CallFilesModal', () => {
});

it('should track analytics event with loading state', () => {
mockStoreState = {
...mockStoreState,
setMockStoreState({
isLoadingFiles: true,
};
});

render(<CallFilesModal {...defaultProps} isOpen={true} callId="test-call-456" />);

Expand All @@ -670,10 +659,9 @@ describe('CallFilesModal', () => {
});

it('should track analytics event with error state', () => {
mockStoreState = {
...mockStoreState,
setMockStoreState({
errorFiles: 'Failed to load files',
};
});

render(<CallFilesModal {...defaultProps} isOpen={true} callId="test-call-error" />);

Expand All @@ -687,10 +675,9 @@ describe('CallFilesModal', () => {
});

it('should track analytics event with no files', () => {
mockStoreState = {
...mockStoreState,
setMockStoreState({
callFiles: [],
};
});

render(<CallFilesModal {...defaultProps} isOpen={true} callId="test-call-no-files" />);

Expand Down
Loading