Add BLE scanning cleanup on screen blur
Implement proper cleanup of BLE scanning operations when users navigate away from the add-sensor screen to prevent resource waste and potential issues. Changes: - Add useFocusEffect hook to stop BLE scan when screen loses focus - Remove unused imports (Device, WPDevice, connectDevice, etc.) - Add comprehensive tests for BLE cleanup behavior - Add tests for screen unmount/blur scenarios 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
deddd3d5bc
commit
a30769387f
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@ -10,11 +10,9 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
||||||
import * as Device from 'expo-device';
|
|
||||||
import { useBLE } from '@/contexts/BLEContext';
|
import { useBLE } from '@/contexts/BLEContext';
|
||||||
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
import type { WPDevice } from '@/services/ble';
|
|
||||||
import {
|
import {
|
||||||
AppColors,
|
AppColors,
|
||||||
BorderRadius,
|
BorderRadius,
|
||||||
@ -34,21 +32,33 @@ export default function AddSensorScreen() {
|
|||||||
isBLEAvailable,
|
isBLEAvailable,
|
||||||
scanDevices,
|
scanDevices,
|
||||||
stopScan,
|
stopScan,
|
||||||
connectDevice,
|
|
||||||
} = useBLE();
|
} = useBLE();
|
||||||
|
|
||||||
const [selectedDevices, setSelectedDevices] = useState<Set<string>>(new Set());
|
const [selectedDevices, setSelectedDevices] = useState<Set<string>>(new Set());
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
|
|
||||||
const beneficiaryName = currentBeneficiary?.name || 'this person';
|
|
||||||
|
|
||||||
// Select all devices by default when scan completes
|
// Select all devices by default when scan completes
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (foundDevices.length > 0 && !isScanning) {
|
if (foundDevices.length > 0 && !isScanning) {
|
||||||
setSelectedDevices(new Set(foundDevices.map(d => d.id)));
|
setSelectedDevices(new Set(foundDevices.map(d => d.id)));
|
||||||
}
|
}
|
||||||
}, [foundDevices, isScanning]);
|
}, [foundDevices, isScanning]);
|
||||||
|
|
||||||
|
// Cleanup: Stop scan when screen loses focus or component unmounts
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
console.log('[AddSensor] Screen focused');
|
||||||
|
|
||||||
|
// Cleanup function - called when screen loses focus
|
||||||
|
return () => {
|
||||||
|
console.log('[AddSensor] Screen blur - cleaning up BLE scan');
|
||||||
|
if (isScanning) {
|
||||||
|
console.log('[AddSensor] Stopping active scan');
|
||||||
|
stopScan();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isScanning, stopScan])
|
||||||
|
);
|
||||||
|
|
||||||
const toggleDeviceSelection = (deviceId: string) => {
|
const toggleDeviceSelection = (deviceId: string) => {
|
||||||
setSelectedDevices(prev => {
|
setSelectedDevices(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
|
|||||||
185
app/(tabs)/beneficiaries/__tests__/add-sensor.cleanup.test.tsx
Normal file
185
app/(tabs)/beneficiaries/__tests__/add-sensor.cleanup.test.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* Add Sensor Screen - BLE Cleanup Tests
|
||||||
|
* Tests that BLE scanning stops when user navigates away from the screen
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { render, waitFor } from '@testing-library/react-native';
|
||||||
|
import { useLocalSearchParams, useFocusEffect } from 'expo-router';
|
||||||
|
import AddSensorScreen from '../[id]/add-sensor';
|
||||||
|
import { useBLE } from '@/contexts/BLEContext';
|
||||||
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
useLocalSearchParams: jest.fn(),
|
||||||
|
router: {
|
||||||
|
push: jest.fn(),
|
||||||
|
back: jest.fn(),
|
||||||
|
},
|
||||||
|
useFocusEffect: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/contexts/BLEContext', () => ({
|
||||||
|
useBLE: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/contexts/BeneficiaryContext', () => ({
|
||||||
|
useBeneficiary: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('expo-device', () => ({
|
||||||
|
isDevice: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AddSensorScreen - BLE cleanup', () => {
|
||||||
|
const mockStopScan = jest.fn();
|
||||||
|
const mockScanDevices = jest.fn();
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock BLE context
|
||||||
|
(useBLE as jest.Mock).mockReturnValue({
|
||||||
|
foundDevices: [],
|
||||||
|
isScanning: false,
|
||||||
|
connectedDevices: new Set(),
|
||||||
|
isBLEAvailable: true,
|
||||||
|
error: null,
|
||||||
|
scanDevices: mockScanDevices,
|
||||||
|
stopScan: mockStopScan,
|
||||||
|
connectDevice: jest.fn(),
|
||||||
|
disconnectDevice: jest.fn(),
|
||||||
|
getWiFiList: jest.fn(),
|
||||||
|
setWiFi: jest.fn(),
|
||||||
|
getCurrentWiFi: jest.fn(),
|
||||||
|
rebootDevice: jest.fn(),
|
||||||
|
cleanupBLE: jest.fn(),
|
||||||
|
clearError: jest.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register cleanup handler with useFocusEffect', () => {
|
||||||
|
render(<AddSensorScreen />);
|
||||||
|
|
||||||
|
expect(useFocusEffect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop scan when cleanup handler is called while scanning', () => {
|
||||||
|
// Mock scanning state
|
||||||
|
(useBLE as jest.Mock).mockReturnValue({
|
||||||
|
foundDevices: [],
|
||||||
|
isScanning: true, // Scan in progress
|
||||||
|
connectedDevices: new Set(),
|
||||||
|
isBLEAvailable: true,
|
||||||
|
error: null,
|
||||||
|
scanDevices: mockScanDevices,
|
||||||
|
stopScan: mockStopScan,
|
||||||
|
connectDevice: jest.fn(),
|
||||||
|
disconnectDevice: jest.fn(),
|
||||||
|
getWiFiList: jest.fn(),
|
||||||
|
setWiFi: jest.fn(),
|
||||||
|
getCurrentWiFi: jest.fn(),
|
||||||
|
rebootDevice: jest.fn(),
|
||||||
|
cleanupBLE: jest.fn(),
|
||||||
|
clearError: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<AddSensorScreen />);
|
||||||
|
|
||||||
|
// Get the cleanup function from useFocusEffect
|
||||||
|
const focusEffectCall = (useFocusEffect as jest.Mock).mock.calls[0][0];
|
||||||
|
const cleanupFn = focusEffectCall(); // Call the effect to get cleanup function
|
||||||
|
|
||||||
|
// Simulate screen blur by calling cleanup
|
||||||
|
if (cleanupFn) {
|
||||||
|
cleanupFn();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockStopScan).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not stop scan when cleanup handler is called and not scanning', () => {
|
||||||
|
// Mock idle state
|
||||||
|
(useBLE as jest.Mock).mockReturnValue({
|
||||||
|
foundDevices: [],
|
||||||
|
isScanning: false, // Not scanning
|
||||||
|
connectedDevices: new Set(),
|
||||||
|
isBLEAvailable: true,
|
||||||
|
error: null,
|
||||||
|
scanDevices: mockScanDevices,
|
||||||
|
stopScan: mockStopScan,
|
||||||
|
connectDevice: jest.fn(),
|
||||||
|
disconnectDevice: jest.fn(),
|
||||||
|
getWiFiList: jest.fn(),
|
||||||
|
setWiFi: jest.fn(),
|
||||||
|
getCurrentWiFi: jest.fn(),
|
||||||
|
rebootDevice: jest.fn(),
|
||||||
|
cleanupBLE: jest.fn(),
|
||||||
|
clearError: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<AddSensorScreen />);
|
||||||
|
|
||||||
|
// Get the cleanup function from useFocusEffect
|
||||||
|
const focusEffectCall = (useFocusEffect as jest.Mock).mock.calls[0][0];
|
||||||
|
const cleanupFn = focusEffectCall();
|
||||||
|
|
||||||
|
// Simulate screen blur by calling cleanup
|
||||||
|
if (cleanupFn) {
|
||||||
|
cleanupFn();
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopScan should not be called when not scanning
|
||||||
|
expect(mockStopScan).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cleanup when component unmounts during active scan', () => {
|
||||||
|
(useBLE as jest.Mock).mockReturnValue({
|
||||||
|
foundDevices: [
|
||||||
|
{ id: 'device-1', name: 'WP_497_81a14c', mac: '81A14C', rssi: -55, wellId: 497 },
|
||||||
|
],
|
||||||
|
isScanning: true,
|
||||||
|
connectedDevices: new Set(),
|
||||||
|
isBLEAvailable: true,
|
||||||
|
error: null,
|
||||||
|
scanDevices: mockScanDevices,
|
||||||
|
stopScan: mockStopScan,
|
||||||
|
connectDevice: jest.fn(),
|
||||||
|
disconnectDevice: jest.fn(),
|
||||||
|
getWiFiList: jest.fn(),
|
||||||
|
setWiFi: jest.fn(),
|
||||||
|
getCurrentWiFi: jest.fn(),
|
||||||
|
rebootDevice: jest.fn(),
|
||||||
|
cleanupBLE: jest.fn(),
|
||||||
|
clearError: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmount } = render(<AddSensorScreen />);
|
||||||
|
|
||||||
|
// Get the cleanup function
|
||||||
|
const focusEffectCall = (useFocusEffect as jest.Mock).mock.calls[0][0];
|
||||||
|
const cleanupFn = focusEffectCall();
|
||||||
|
|
||||||
|
// Unmount component
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
// Cleanup should have been called during unmount
|
||||||
|
if (cleanupFn) {
|
||||||
|
cleanupFn();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockStopScan).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
182
services/ble/__tests__/BLEContext.cleanup.test.tsx
Normal file
182
services/ble/__tests__/BLEContext.cleanup.test.tsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* BLE Context Cleanup Tests
|
||||||
|
* Tests for BLE scanning cleanup when screens lose focus
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { renderHook, act, waitFor } from '@testing-library/react-native';
|
||||||
|
import { BLEProvider, useBLE } from '@/contexts/BLEContext';
|
||||||
|
import * as bleModule from '@/services/ble';
|
||||||
|
|
||||||
|
// Mock the BLE service
|
||||||
|
jest.mock('@/services/ble', () => ({
|
||||||
|
bleManager: {
|
||||||
|
scanDevices: jest.fn(),
|
||||||
|
stopScan: jest.fn(),
|
||||||
|
connectDevice: jest.fn(),
|
||||||
|
disconnectDevice: jest.fn(),
|
||||||
|
getWiFiList: jest.fn(),
|
||||||
|
setWiFi: jest.fn(),
|
||||||
|
getCurrentWiFi: jest.fn(),
|
||||||
|
rebootDevice: jest.fn(),
|
||||||
|
cleanup: jest.fn(),
|
||||||
|
},
|
||||||
|
isBLEAvailable: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('BLEContext cleanup behavior', () => {
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<BLEProvider>{children}</BLEProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop scan when cleanupBLE is called while scanning', async () => {
|
||||||
|
const mockScanDevices = jest.fn().mockResolvedValue([
|
||||||
|
{ id: 'device-1', name: 'WP_497_81a14c', mac: '81A14C', rssi: -55, wellId: 497 },
|
||||||
|
]);
|
||||||
|
const mockStopScan = jest.fn();
|
||||||
|
|
||||||
|
(bleModule.bleManager.scanDevices as jest.Mock) = mockScanDevices;
|
||||||
|
(bleModule.bleManager.stopScan as jest.Mock) = mockStopScan;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBLE(), { wrapper });
|
||||||
|
|
||||||
|
// Start scanning
|
||||||
|
act(() => {
|
||||||
|
result.current.scanDevices();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isScanning).toBe(true);
|
||||||
|
|
||||||
|
// Cleanup while scanning
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.cleanupBLE();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockStopScan).toHaveBeenCalled();
|
||||||
|
expect(result.current.isScanning).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear found devices when cleanupBLE is called', async () => {
|
||||||
|
const mockScanDevices = jest.fn().mockResolvedValue([
|
||||||
|
{ id: 'device-1', name: 'WP_497_81a14c', mac: '81A14C', rssi: -55, wellId: 497 },
|
||||||
|
{ id: 'device-2', name: 'WP_498_82b25d', mac: '82B25D', rssi: -60, wellId: 498 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
(bleModule.bleManager.scanDevices as jest.Mock) = mockScanDevices;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBLE(), { wrapper });
|
||||||
|
|
||||||
|
// Scan and find devices
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.scanDevices();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.foundDevices).toHaveLength(2);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.cleanupBLE();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.foundDevices).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear connected devices when cleanupBLE is called', async () => {
|
||||||
|
const mockConnectDevice = jest.fn().mockResolvedValue(true);
|
||||||
|
const mockCleanup = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
(bleModule.bleManager.connectDevice as jest.Mock) = mockConnectDevice;
|
||||||
|
(bleModule.bleManager.cleanup as jest.Mock) = mockCleanup;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBLE(), { wrapper });
|
||||||
|
|
||||||
|
// Connect to a device
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.connectDevice('device-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.connectedDevices.size).toBe(1);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.cleanupBLE();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockCleanup).toHaveBeenCalled();
|
||||||
|
expect(result.current.connectedDevices.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear errors when cleanupBLE is called', async () => {
|
||||||
|
const mockScanDevices = jest.fn().mockRejectedValue(new Error('Scan failed'));
|
||||||
|
|
||||||
|
(bleModule.bleManager.scanDevices as jest.Mock) = mockScanDevices;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBLE(), { wrapper });
|
||||||
|
|
||||||
|
// Trigger an error
|
||||||
|
await act(async () => {
|
||||||
|
try {
|
||||||
|
await result.current.scanDevices();
|
||||||
|
} catch {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.error).toBeTruthy();
|
||||||
|
|
||||||
|
// Cleanup should clear errors
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.cleanupBLE();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if cleanup fails', async () => {
|
||||||
|
const mockCleanup = jest.fn().mockRejectedValue(new Error('Cleanup failed'));
|
||||||
|
|
||||||
|
(bleModule.bleManager.cleanup as jest.Mock) = mockCleanup;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBLE(), { wrapper });
|
||||||
|
|
||||||
|
// Cleanup should not throw even if bleManager.cleanup fails
|
||||||
|
await expect(
|
||||||
|
act(async () => {
|
||||||
|
await result.current.cleanupBLE();
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop scan before calling bleManager.cleanup', async () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
|
||||||
|
const mockStopScan = jest.fn(() => {
|
||||||
|
callOrder.push('stopScan');
|
||||||
|
});
|
||||||
|
const mockCleanup = jest.fn().mockResolvedValue(undefined).mockImplementation(() => {
|
||||||
|
callOrder.push('cleanup');
|
||||||
|
});
|
||||||
|
|
||||||
|
(bleModule.bleManager.stopScan as jest.Mock) = mockStopScan;
|
||||||
|
(bleModule.bleManager.cleanup as jest.Mock) = mockCleanup;
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBLE(), { wrapper });
|
||||||
|
|
||||||
|
// Start scanning
|
||||||
|
act(() => {
|
||||||
|
(result.current as any).setIsScanning?.(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger cleanup
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.cleanupBLE();
|
||||||
|
});
|
||||||
|
|
||||||
|
// stopScan should be called before cleanup
|
||||||
|
expect(callOrder).toEqual(['stopScan', 'cleanup']);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user