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 {
|
||||
View,
|
||||
Text,
|
||||
@ -10,11 +10,9 @@ import {
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import * as Device from 'expo-device';
|
||||
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
||||
import { useBLE } from '@/contexts/BLEContext';
|
||||
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||
import type { WPDevice } from '@/services/ble';
|
||||
import {
|
||||
AppColors,
|
||||
BorderRadius,
|
||||
@ -34,21 +32,33 @@ export default function AddSensorScreen() {
|
||||
isBLEAvailable,
|
||||
scanDevices,
|
||||
stopScan,
|
||||
connectDevice,
|
||||
} = useBLE();
|
||||
|
||||
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
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (foundDevices.length > 0 && !isScanning) {
|
||||
setSelectedDevices(new Set(foundDevices.map(d => d.id)));
|
||||
}
|
||||
}, [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) => {
|
||||
setSelectedDevices(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