/** * Equipment Screen Tests * Tests sensor list display, status calculation, and user interactions */ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import { useLocalSearchParams, router } from 'expo-router'; import EquipmentScreen from '../[id]/equipment'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { useBLE } from '@/contexts/BLEContext'; import { api } from '@/services/api'; // Mock Alert const mockAlert = jest.fn(); jest.mock('react-native/Libraries/Alert/Alert', () => ({ alert: mockAlert, })); // Mock dependencies jest.mock('expo-router', () => ({ useLocalSearchParams: jest.fn(), router: { push: jest.fn(), back: jest.fn(), }, })); jest.mock('@/contexts/BeneficiaryContext', () => ({ useBeneficiary: jest.fn(), })); jest.mock('@/contexts/BLEContext', () => ({ useBLE: jest.fn(), })); jest.mock('@/services/api', () => ({ api: { getDevicesForBeneficiary: jest.fn(), detachDeviceFromBeneficiary: jest.fn(), }, ROOM_LOCATIONS: [ { id: 'bedroom', label: 'Bedroom', icon: '🛏️' }, { id: 'bathroom', label: 'Bathroom', icon: '🚿' }, { id: 'kitchen', label: 'Kitchen', icon: '🍳' }, ], })); jest.mock('expo-device', () => ({ isDevice: true, })); describe('EquipmentScreen', () => { const mockSensors = [ { deviceId: 'device-1', name: 'WP_497_81a14c', wellId: 497, mac: '81A14C', status: 'online' as const, lastSeen: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago location: 'bedroom', source: 'api' as const, }, { deviceId: 'device-2', name: 'WP_498_82b25d', wellId: 498, mac: '82B25D', status: 'warning' as const, lastSeen: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago location: undefined, source: 'api' as const, }, { deviceId: 'device-3', name: 'WP_499_83c36e', wellId: 499, mac: '83C36E', status: 'offline' as const, lastSeen: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago location: 'kitchen', source: 'api' as const, }, ]; beforeEach(() => { jest.clearAllMocks(); // Mock route params (useLocalSearchParams as jest.Mock).mockReturnValue({ id: '1' }); // Mock beneficiary context (useBeneficiary as jest.Mock).mockReturnValue({ currentBeneficiary: { id: 1, name: 'Maria', role: 'owner', }, }); // Mock BLE context (useBLE as jest.Mock).mockReturnValue({ isBLEAvailable: true, }); // Mock API - return sensors by default (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: mockSensors, }); (api.detachDeviceFromBeneficiary as jest.Mock).mockResolvedValue({ ok: true, }); }); describe('Sensor List Display', () => { it('should display loading state initially', () => { const { getByText } = render(); expect(getByText('Loading sensors...')).toBeTruthy(); }); it('should display sensor list after loading', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Verify sensors are displayed expect(getByText('WP_497_81a14c')).toBeTruthy(); expect(getByText('WP_498_82b25d')).toBeTruthy(); expect(getByText('WP_499_83c36e')).toBeTruthy(); }); it('should display correct sensor count in summary', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Summary should show 3 total, 1 online, 1 warning, 1 offline expect(getByText('3')).toBeTruthy(); // Total expect(getByText('1')).toBeTruthy(); // Each status count }); it('should display status badges correctly', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText('Online')).toBeTruthy(); expect(getByText('Warning')).toBeTruthy(); expect(getByText('Offline')).toBeTruthy(); }); it('should display formatted last seen time', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Check for time formats expect(getByText(/\d+ min ago/)).toBeTruthy(); expect(getByText(/\d+ hour/)).toBeTruthy(); }); it('should display location for sensors with location set', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Bedroom location should show with icon expect(getByText(/Bedroom/)).toBeTruthy(); expect(getByText(/Kitchen/)).toBeTruthy(); // Sensor without location should show placeholder expect(getByText('No location set')).toBeTruthy(); }); }); describe('Empty State', () => { it('should display empty state when no sensors', async () => { (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: [], }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText('No Sensors Connected')).toBeTruthy(); expect(getByText(/Add WP sensors/)).toBeTruthy(); expect(getByText('Add Sensors')).toBeTruthy(); }); }); describe('Error Handling', () => { it('should display error message on API failure', async () => { (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: false, error: { message: 'Network error' }, }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText('Network error')).toBeTruthy(); expect(getByText('Try Again')).toBeTruthy(); }); it('should retry on error button press', async () => { (api.getDevicesForBeneficiary as jest.Mock) .mockResolvedValueOnce({ ok: false, error: { message: 'Network error' }, }) .mockResolvedValueOnce({ ok: true, data: mockSensors, }); const { getByText, queryByText } = render(); await waitFor(() => { expect(getByText('Network error')).toBeTruthy(); }); fireEvent.press(getByText('Try Again')); await waitFor(() => { expect(queryByText('Network error')).toBeNull(); expect(getByText('WP_497_81a14c')).toBeTruthy(); }); }); }); describe('Navigation', () => { it('should navigate to add-sensor screen when Add Sensors is pressed', async () => { (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: [], }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); fireEvent.press(getByText('Add Sensors')); expect(router.push).toHaveBeenCalledWith('/(tabs)/beneficiaries/1/add-sensor'); }); it('should navigate to add-sensor screen when Add More Sensors is pressed', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); fireEvent.press(getByText('Add More Sensors')); expect(router.push).toHaveBeenCalledWith('/(tabs)/beneficiaries/1/add-sensor'); }); it('should navigate back when back button is pressed', async () => { const { getByTestId, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Find and press back button (arrow-back icon) const backButton = getByTestId ? getByTestId('back-button') : null; if (backButton) { fireEvent.press(backButton); expect(router.back).toHaveBeenCalled(); } }); }); describe('Sensor Interactions', () => { it('should show action sheet when offline sensor is pressed', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Tap on offline sensor fireEvent.press(getByText('WP_499_83c36e')); // Alert should be shown with options await waitFor(() => { expect(mockAlert).toHaveBeenCalled(); }); }); it('should navigate to device settings when online sensor settings button is pressed', async () => { const { getByText, queryByText, getAllByTestId } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // The settings button should navigate to device settings // Note: This depends on having testID on the button }); }); describe('Detach Sensor', () => { it('should show confirmation dialog when detach is triggered', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Trigger detach (would need to find the detach button) // This is typically done through the settings or action sheet }); it('should remove sensor from list after successful detach', async () => { const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Verify sensor exists before detach expect(getByText('WP_497_81a14c')).toBeTruthy(); // After detach, sensor should be removed // Note: This requires triggering the detach flow }); it('should show error alert on detach failure', async () => { (api.detachDeviceFromBeneficiary as jest.Mock).mockResolvedValue({ ok: false, error: { message: 'Failed to detach' }, }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Trigger detach and verify error handling }); }); describe('Simulator Mode', () => { it('should show simulator warning when BLE is not available', async () => { (useBLE as jest.Mock).mockReturnValue({ isBLEAvailable: false, }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText(/Simulator/)).toBeTruthy(); expect(getByText(/mock data/i)).toBeTruthy(); }); it('should not show simulator warning when BLE is available', async () => { (useBLE as jest.Mock).mockReturnValue({ isBLEAvailable: true, }); const { queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(queryByText(/Simulator/)).toBeNull(); }); }); describe('Refresh', () => { it('should reload sensors on pull to refresh', async () => { const { getByTestId, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); // Clear mock calls (api.getDevicesForBeneficiary as jest.Mock).mockClear(); // Trigger refresh (would need RefreshControl testID) // fireEvent(getByTestId('scroll-view'), 'refresh'); // Verify API was called again // await waitFor(() => { // expect(api.getDevicesForBeneficiary).toHaveBeenCalledTimes(1); // }); }); }); }); describe('Sensor Status Calculation', () => { // These tests verify the status calculation logic based on lastSeen it('should show online status for sensors seen less than 5 minutes ago', async () => { const recentSensor = { deviceId: 'device-recent', name: 'WP_500', wellId: 500, mac: '84D47F', status: 'online' as const, lastSeen: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago source: 'api' as const, }; (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: [recentSensor], }); (useLocalSearchParams as jest.Mock).mockReturnValue({ id: '1' }); (useBeneficiary as jest.Mock).mockReturnValue({ currentBeneficiary: { id: 1, name: 'Maria' }, }); (useBLE as jest.Mock).mockReturnValue({ isBLEAvailable: true }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText('Online')).toBeTruthy(); }); it('should show warning status for sensors seen 5-60 minutes ago', async () => { const warningSensor = { deviceId: 'device-warning', name: 'WP_501', wellId: 501, mac: '85E58G', status: 'warning' as const, lastSeen: new Date(Date.now() - 30 * 60 * 1000), // 30 minutes ago source: 'api' as const, }; (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: [warningSensor], }); (useLocalSearchParams as jest.Mock).mockReturnValue({ id: '1' }); (useBeneficiary as jest.Mock).mockReturnValue({ currentBeneficiary: { id: 1, name: 'Maria' }, }); (useBLE as jest.Mock).mockReturnValue({ isBLEAvailable: true }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText('Warning')).toBeTruthy(); }); it('should show offline status for sensors seen more than 60 minutes ago', async () => { const offlineSensor = { deviceId: 'device-offline', name: 'WP_502', wellId: 502, mac: '86F69H', status: 'offline' as const, lastSeen: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago source: 'api' as const, }; (api.getDevicesForBeneficiary as jest.Mock).mockResolvedValue({ ok: true, data: [offlineSensor], }); (useLocalSearchParams as jest.Mock).mockReturnValue({ id: '1' }); (useBeneficiary as jest.Mock).mockReturnValue({ currentBeneficiary: { id: 1, name: 'Maria' }, }); (useBLE as jest.Mock).mockReturnValue({ isBLEAvailable: true }); const { getByText, queryByText } = render(); await waitFor(() => { expect(queryByText('Loading sensors...')).toBeNull(); }); expect(getByText('Offline')).toBeTruthy(); }); });