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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"react-native-webview": "13.16.1",
"react-native-worklets": "0.8.3",
"react-query-kit": "3.3.2",
"sanitize-html": "2.17.0",
"tailwind-variants": "0.2.1",
"zod": "3.23.8",
"zustand": "4.5.7"
Expand All @@ -171,6 +172,7 @@
"@types/mapbox-gl": "3.4.1",
"@types/react": "~19.2.14",
"@types/react-native-base64": "0.2.2",
"@types/sanitize-html": "^2.16.0",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"babel-jest": "30.0.5",
Expand Down
4 changes: 2 additions & 2 deletions src/app/call/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ export default function CallDetail() {
{callExtraData?.Protocols && callExtraData.Protocols.length > 0 ? (
<VStack className="space-y-3">
{callExtraData.Protocols.map((protocol, index) => (
<Box key={index} className="rounded-lg bg-gray-50 p-3">
<Box key={index} className="rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<Text className="font-semibold">{protocol.Name}</Text>
<Text className="text-sm text-gray-600">{protocol.Description}</Text>
<Text className="text-sm text-gray-600 dark:text-gray-400">{protocol.Description}</Text>
<Box>
<HtmlRenderer html={protocol.ProtocolText ?? ''} style={StyleSheet.flatten([styles.container, { height: 200 }])} />
</Box>
Expand Down
105 changes: 90 additions & 15 deletions src/components/calls/__tests__/close-call-bottom-sheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,45 @@ jest.mock('react-native-keyboard-controller', () => ({
},
}));

// Mock lucide icons
jest.mock('lucide-react-native', () => ({
ChevronDown: () => null,
}));

// Mock UI components
jest.mock('@/components/ui/actionsheet', () => {
const { View } = require('react-native');
return {
Actionsheet: ({ isOpen, children, testID }: any) => (isOpen ? <View testID={testID ?? 'actionsheet'}>{children}</View> : null),
ActionsheetBackdrop: ({ children }: any) => <View testID="actionsheet-backdrop">{children}</View>,
ActionsheetContent: ({ children, style }: any) => (
<View testID="actionsheet-content" style={style}>
{children}
</View>
),
ActionsheetDragIndicator: () => <View testID="actionsheet-drag-indicator" />,
ActionsheetDragIndicatorWrapper: ({ children }: any) => <View testID="actionsheet-drag-indicator-wrapper">{children}</View>,
};
});

jest.mock('@/components/ui/button', () => ({
Button: ({ children, onPress, testID, disabled, ...props }: any) => {
Button: ({ children, onPress, testID, disabled, isDisabled, ...props }: any) => {
const { TouchableOpacity } = require('react-native');
return <TouchableOpacity onPress={onPress} testID={testID} disabled={disabled} {...props}>{children}</TouchableOpacity>;
const resolvedDisabled = disabled ?? isDisabled;
return (
<TouchableOpacity onPress={onPress} testID={testID} disabled={resolvedDisabled} accessibilityState={{ disabled: !!resolvedDisabled }} {...props}>
{children}
</TouchableOpacity>
);
},
ButtonText: ({ children, ...props }: any) => {
const { Text } = require('react-native');
return <Text {...props}>{children}</Text>;
},
}));

jest.mock('@/components/ui/heading', () => ({
Heading: ({ children, ...props }: any) => {
const { Text } = require('react-native');
return <Text {...props}>{children}</Text>;
},
}));

jest.mock('@/components/ui/text', () => ({
Text: ({ children, ...props }: any) => {
const { Text: RNText } = require('react-native');
Expand All @@ -87,6 +109,58 @@ jest.mock('@/components/ui/hstack', () => ({
},
}));

jest.mock('@/components/ui/form-control', () => ({
FormControl: ({ children, ...props }: any) => {
const { View } = require('react-native');
return <View {...props}>{children}</View>;
},
FormControlLabel: ({ children, ...props }: any) => {
const { View } = require('react-native');
return <View {...props}>{children}</View>;
},
FormControlLabelText: ({ children, ...props }: any) => {
const { Text } = require('react-native');
return <Text {...props}>{children}</Text>;
},
}));

jest.mock('@/components/ui/select', () => ({
Select: ({ children, testID, onValueChange, ...props }: any) => {
const { View } = require('react-native');
return (
<View testID={testID} onValueChange={onValueChange} {...props}>
{children}
</View>
);
},
SelectTrigger: ({ children, ...props }: any) => {
const { View } = require('react-native');
return <View {...props}>{children}</View>;
},
SelectInput: ({ placeholder, ...props }: any) => {
const { Text } = require('react-native');
return <Text {...props}>{placeholder}</Text>;
},
SelectIcon: () => null,
SelectPortal: ({ children, ...props }: any) => {
const { View } = require('react-native');
return <View {...props}>{children}</View>;
},
SelectBackdrop: () => null,
SelectContent: ({ children, ...props }: any) => {
const { View } = require('react-native');
return <View {...props}>{children}</View>;
},
SelectItem: ({ label, ...props }: any) => {
const { View, Text } = require('react-native');
return (
<View {...props}>
<Text>{label}</Text>
</View>
);
},
}));

jest.mock('@/components/ui/textarea', () => ({
Textarea: ({ children, ...props }: any) => {
const { View } = require('react-native');
Expand Down Expand Up @@ -117,12 +191,10 @@ const mockUseCallDetailStore = useCallDetailStore as jest.MockedFunction<typeof
const mockUseCallsStore = useCallsStore as jest.MockedFunction<typeof useCallsStore>;
const mockUseToastStore = useToastStore as jest.MockedFunction<typeof useToastStore>;

/** Helper: select a close call type via the inline dropdown */
/** Helper: select a close call type via the gluestack Select's onValueChange */
function selectCloseCallType(type: string) {
const typeSelect = screen.getByTestId('close-call-type-select');
fireEvent.press(typeSelect);
const option = screen.getByTestId(`close-call-type-option-${type}`);
fireEvent.press(option);
fireEvent(typeSelect, 'onValueChange', type);
}

describe('CloseCallBottomSheet', () => {
Expand Down Expand Up @@ -168,6 +240,7 @@ describe('CloseCallBottomSheet', () => {
expect(screen.getByText('call_detail.close_call_type')).toBeTruthy();
expect(screen.getByText('call_detail.close_call_note')).toBeTruthy();
expect(screen.getByText('common.cancel')).toBeTruthy();
expect(screen.getByTestId('close-call-bottom-sheet')).toBeTruthy();
});

it('should show error toast when no close type is selected', async () => {
Expand All @@ -191,7 +264,7 @@ describe('CloseCallBottomSheet', () => {
const mockOnClose = jest.fn();
render(<CloseCallBottomSheet isOpen={true} onClose={mockOnClose} callId="test-call-1" />);

// Select close type via inline dropdown
// Select close type
selectCloseCallType('1');

// Add note
Expand Down Expand Up @@ -277,7 +350,7 @@ describe('CloseCallBottomSheet', () => {

render(<CloseCallBottomSheet isOpen={true} onClose={jest.fn()} callId="test-call-1" />);

// Select close type via inline dropdown
// Select close type
selectCloseCallType(type);

// Submit
Expand Down Expand Up @@ -316,8 +389,10 @@ describe('CloseCallBottomSheet', () => {
fireEvent.press(submitButton);

// Buttons should be disabled while submitting
expect(submitButton).toBeDisabled();
expect(cancelButton).toBeDisabled();
await waitFor(() => {
expect(cancelButton).toBeDisabled();
});
expect(screen.getByText('common.submitting')).toBeTruthy();

// Resolve the promise to complete the test
resolveCloseCall!();
Expand Down
11 changes: 7 additions & 4 deletions src/components/calls/call-card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AlertTriangle, ClipboardList, MapPin, Phone, Timer } from 'lucide-react-native';
import React, { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Animated, ScrollView, StyleSheet } from 'react-native';
import { Animated, Platform, ScrollView, StyleSheet } from 'react-native';

import { Box } from '@/components/ui/box';
import { Button, ButtonIcon, ButtonText } from '@/components/ui/button';
Expand Down Expand Up @@ -181,11 +181,14 @@ export const CallCard: React.FC<CallCardProps> = ({ call, priority, showTimerIco
</VStack>

{/* Nature of Call */}
{call.Nature && (
<Box className="mt-4 rounded-lg bg-white/50 p-3">
{call.Nature ? (
// Android's WebView claims the touch stream (requestDisallowInterceptTouchEvent),
// so a drag starting on it never reaches the surrounding list — kill its pointer
// events there and let the list scroll. iOS nests scrolling fine, leave it alone.
<Box className="mt-4 rounded-lg bg-white/50 p-3" pointerEvents={Platform.OS === 'android' ? 'none' : 'auto'}>
<HtmlRenderer html={call.Nature} style={StyleSheet.flatten([styles.container, { height: 80 }])} textColor={textColor} />
</Box>
)}
) : null}

{/* Start Command action — opens (or creates) the IC board for this call.
Multiple boards can exist at once; the active one shows a badge. */}
Expand Down
Loading
Loading