/** * Tests for BLEScanner component */ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import { BLEScanner, SelectionMode } from '../BLEScanner'; import { WPDevice } from '@/services/ble/types'; // Mock the BLE context const mockScanDevices = jest.fn(); const mockStopScan = jest.fn(); const mockClearError = jest.fn(); const mockBLEContext = { foundDevices: [] as WPDevice[], isScanning: false, connectedDevices: new Set(), isBLEAvailable: true, error: null as string | null, permissionError: false, scanDevices: mockScanDevices, stopScan: mockStopScan, clearError: mockClearError, }; jest.mock('@/contexts/BLEContext', () => ({ useBLE: () => mockBLEContext, })); // Mock WiFiSignalIndicator to avoid native dependencies jest.mock('@/components/WiFiSignalIndicator', () => ({ WiFiSignalIndicator: () => null, getSignalStrengthLabel: (rssi: number) => { if (rssi >= -50) return 'Excellent'; if (rssi >= -60) return 'Good'; if (rssi >= -70) return 'Fair'; return 'Weak'; }, getSignalStrengthColor: () => '#10B981', })); // Mock Linking jest.mock('react-native/Libraries/Linking/Linking', () => ({ openSettings: jest.fn(), })); const mockDevices: WPDevice[] = [ { id: 'device-1', name: 'WP_497_81a14c', mac: '142B2F81A14C', rssi: -55, wellId: 497, }, { id: 'device-2', name: 'WP_523_81aad4', mac: '142B2F81AAD4', rssi: -67, wellId: 523, }, { id: 'device-3', name: 'WP_600_abc123', mac: '142B2FABC123', rssi: -75, wellId: 600, }, ]; describe('BLEScanner', () => { beforeEach(() => { jest.clearAllMocks(); // Reset mock context mockBLEContext.foundDevices = []; mockBLEContext.isScanning = false; mockBLEContext.isBLEAvailable = true; mockBLEContext.error = null; mockBLEContext.permissionError = false; mockScanDevices.mockResolvedValue(undefined); }); describe('Initial State', () => { it('renders scan button when no devices are found', () => { const { getByTestId } = render(); expect(getByTestId('scan-button')).toBeTruthy(); }); it('shows instructions when showInstructions is true', () => { const { getByText } = render( ); expect(getByText('Scanning for Sensors')).toBeTruthy(); expect(getByText('Make sure sensors are powered on')).toBeTruthy(); }); it('shows simulator warning when BLE is not available', () => { mockBLEContext.isBLEAvailable = false; const { getByText } = render(); expect(getByText('Simulator mode - showing mock sensors')).toBeTruthy(); }); }); describe('Scanning', () => { it('calls scanDevices when scan button is pressed', async () => { const { getByTestId } = render(); fireEvent.press(getByTestId('scan-button')); await waitFor(() => { expect(mockScanDevices).toHaveBeenCalledTimes(1); }); }); it('shows scanning indicator when scanning', () => { mockBLEContext.isScanning = true; const { getByText, getByTestId } = render(); expect(getByText('Scanning for WP sensors...')).toBeTruthy(); expect(getByTestId('stop-scan-button')).toBeTruthy(); }); it('calls stopScan when stop button is pressed', () => { mockBLEContext.isScanning = true; const { getByTestId } = render(); fireEvent.press(getByTestId('stop-scan-button')); expect(mockStopScan).toHaveBeenCalledTimes(1); }); it('clears error before scanning', async () => { const { getByTestId } = render(); fireEvent.press(getByTestId('scan-button')); await waitFor(() => { expect(mockClearError).toHaveBeenCalled(); }); }); }); describe('Device List', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('displays found devices', () => { const { getByText } = render(); expect(getByText('Found Sensors (3)')).toBeTruthy(); expect(getByText('WP_497_81a14c')).toBeTruthy(); expect(getByText('WP_523_81aad4')).toBeTruthy(); expect(getByText('WP_600_abc123')).toBeTruthy(); }); it('shows Well ID for devices', () => { const { getByText } = render(); expect(getByText('Well ID: 497')).toBeTruthy(); expect(getByText('Well ID: 523')).toBeTruthy(); }); it('shows rescan button when devices are displayed', () => { const { getByTestId } = render(); expect(getByTestId('rescan-button')).toBeTruthy(); }); it('rescans when rescan button is pressed', async () => { const { getByTestId } = render(); fireEvent.press(getByTestId('rescan-button')); await waitFor(() => { expect(mockScanDevices).toHaveBeenCalled(); }); }); }); describe('Multiple Selection Mode', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('shows select all button in multiple mode', () => { const { getByTestId } = render( ); expect(getByTestId('select-all-button')).toBeTruthy(); }); it('toggles device selection on press', () => { const onDevicesSelected = jest.fn(); const { getByTestId } = render( ); // Click first device fireEvent.press(getByTestId('device-item-device-1')); expect(onDevicesSelected).toHaveBeenCalled(); }); it('selects all devices when select all is pressed', () => { const onDevicesSelected = jest.fn(); const { getByTestId, getByText } = render( ); // Toggle from "Select All" -> all selected fireEvent.press(getByTestId('select-all-button')); // Now should show "Deselect All" expect(getByText('Deselect All')).toBeTruthy(); }); it('deselects all when deselect all is pressed', () => { const onDevicesSelected = jest.fn(); const { getByTestId, getByText } = render( ); // First select all fireEvent.press(getByTestId('select-all-button')); // Then deselect all fireEvent.press(getByTestId('select-all-button')); expect(getByText('Select All')).toBeTruthy(); }); }); describe('Single Selection Mode', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('does not show select all button in single mode', () => { const { queryByTestId } = render( ); expect(queryByTestId('select-all-button')).toBeNull(); }); it('calls onDeviceSelected when a device is selected', () => { const onDeviceSelected = jest.fn(); const { getByTestId } = render( ); fireEvent.press(getByTestId('device-item-device-1')); expect(onDeviceSelected).toHaveBeenCalledWith( expect.objectContaining({ id: 'device-1', name: 'WP_497_81a14c', }) ); }); it('replaces selection when another device is selected', () => { const onDeviceSelected = jest.fn(); const { getByTestId } = render( ); fireEvent.press(getByTestId('device-item-device-1')); fireEvent.press(getByTestId('device-item-device-2')); // Last call should be with device-2 expect(onDeviceSelected).toHaveBeenLastCalledWith( expect.objectContaining({ id: 'device-2', }) ); }); }); describe('None Selection Mode', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('does not show checkboxes in none mode', () => { const { queryByTestId } = render( ); // Should not have select all button expect(queryByTestId('select-all-button')).toBeNull(); }); it('devices are not selectable in none mode', () => { const onDevicesSelected = jest.fn(); const { getByTestId } = render( ); fireEvent.press(getByTestId('device-item-device-1')); // Should be called with empty array (nothing selected) expect(onDevicesSelected).toHaveBeenCalledWith([]); }); }); describe('Disabled Devices', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('shows disabled badge for disabled devices', () => { const { getByText } = render( ); expect(getByText('Added')).toBeTruthy(); }); it('does not allow selecting disabled devices', () => { const onDevicesSelected = jest.fn(); const { getByTestId } = render( ); // Try to select disabled device fireEvent.press(getByTestId('device-item-device-1')); // Should not include disabled device in selection const lastCall = onDevicesSelected.mock.calls[onDevicesSelected.mock.calls.length - 1]; expect(lastCall[0].find((d: WPDevice) => d.id === 'device-1')).toBeUndefined(); }); it('excludes disabled devices from select all', () => { const onDevicesSelected = jest.fn(); const { getByTestId } = render( ); fireEvent.press(getByTestId('select-all-button')); // Should only have 2 devices (not including device-1) const lastCall = onDevicesSelected.mock.calls[onDevicesSelected.mock.calls.length - 1]; expect(lastCall[0].length).toBe(2); expect(lastCall[0].find((d: WPDevice) => d.id === 'device-1')).toBeUndefined(); }); }); describe('Device Filter', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('applies custom filter to devices', () => { const filter = (device: WPDevice) => device.rssi > -60; const { getByText, queryByText } = render( ); // Only device-1 has rssi > -60 expect(getByText('WP_497_81a14c')).toBeTruthy(); expect(queryByText('WP_523_81aad4')).toBeNull(); expect(queryByText('WP_600_abc123')).toBeNull(); }); it('shows correct count with filter applied', () => { const filter = (device: WPDevice) => device.rssi > -70; const { getByText } = render( ); expect(getByText('Found Sensors (2)')).toBeTruthy(); }); }); describe('Empty State', () => { it('shows empty state after scan with no devices', () => { mockBLEContext.foundDevices = []; // Simulate scan completion const { getByTestId, rerender, getByText } = render(); // Trigger scan fireEvent.press(getByTestId('scan-button')); // Re-render with scan completed mockBLEContext.foundDevices = []; rerender(); // Should show retry button (which appears in empty state after scan) // Note: getByTestId will fail if element not present, so we check the retry button expect(getByTestId('retry-scan-button')).toBeTruthy(); }); it('shows custom empty state message', async () => { const customMessage = 'Custom empty message here'; const { getByTestId, getByText, rerender } = render( ); // Trigger scan fireEvent.press(getByTestId('scan-button')); // Re-render after scan rerender(); await waitFor(() => { expect(getByText(customMessage)).toBeTruthy(); }); }); it('allows retry from empty state', async () => { const { getByTestId, rerender } = render(); // First scan fireEvent.press(getByTestId('scan-button')); rerender(); // Retry fireEvent.press(getByTestId('retry-scan-button')); await waitFor(() => { expect(mockScanDevices).toHaveBeenCalledTimes(2); }); }); }); describe('Error Handling', () => { it('shows permission error with settings button', () => { mockBLEContext.error = 'Bluetooth permissions not granted'; mockBLEContext.permissionError = true; const { getByText } = render(); expect(getByText('Bluetooth permissions not granted')).toBeTruthy(); expect(getByText('Open Settings')).toBeTruthy(); }); it('does not show error banner for non-permission errors', () => { mockBLEContext.error = 'Some other error'; mockBLEContext.permissionError = false; const { queryByText } = render(); // Permission error banner should not be shown expect(queryByText('Open Settings')).toBeNull(); }); }); describe('Compact Mode', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('renders in compact mode', () => { const { getByText } = render(); expect(getByText('WP_497_81a14c')).toBeTruthy(); }); it('shows compact instructions', () => { mockBLEContext.foundDevices = []; const { getByText } = render( ); expect(getByText('Make sure sensors are powered on')).toBeTruthy(); }); }); describe('Auto Scan', () => { it('starts scan automatically when autoScan is true', async () => { render(); await waitFor(() => { expect(mockScanDevices).toHaveBeenCalledTimes(1); }); }); it('does not auto scan when autoScan is false', () => { render(); expect(mockScanDevices).not.toHaveBeenCalled(); }); it('only auto scans once', async () => { const { rerender } = render(); await waitFor(() => { expect(mockScanDevices).toHaveBeenCalledTimes(1); }); // Re-render rerender(); // Should still be 1 expect(mockScanDevices).toHaveBeenCalledTimes(1); }); }); describe('Callbacks', () => { beforeEach(() => { mockBLEContext.foundDevices = mockDevices; }); it('calls onDevicesSelected with selected devices', () => { const onDevicesSelected = jest.fn(); const { getByTestId } = render( ); fireEvent.press(getByTestId('device-item-device-1')); fireEvent.press(getByTestId('device-item-device-2')); expect(onDevicesSelected).toHaveBeenCalled(); const lastCall = onDevicesSelected.mock.calls[onDevicesSelected.mock.calls.length - 1]; expect(lastCall[0].length).toBe(2); }); it('calls both onDeviceSelected and onDevicesSelected in single mode', () => { const onDeviceSelected = jest.fn(); const onDevicesSelected = jest.fn(); const { getByTestId } = render( ); fireEvent.press(getByTestId('device-item-device-1')); expect(onDeviceSelected).toHaveBeenCalled(); expect(onDevicesSelected).toHaveBeenCalled(); }); }); });