WellNuo/services/ble/__tests__/BLEManager.bulk.test.ts
Sergei 5f40370dfa Add unit tests for BLE error handling, bulk operations, and WiFi validation
- BLEErrors.test.ts: Tests for BLEError class, parseBLEError function,
  isBLEError, getErrorInfo, getRecoveryActionLabel, and BLELogger utilities
- BLEManager.bulk.test.ts: Tests for bulkDisconnect, bulkReboot, and
  bulkSetWiFi operations including progress callbacks and edge cases
- BLEManager.wifi.test.ts: Tests for WiFi credential validation (SSID/password),
  error scenarios, getWiFiList, getCurrentWiFi, and signal quality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 10:38:56 -08:00

331 lines
9.9 KiB
TypeScript

/**
* BLE Manager Bulk Operations Unit Tests
* Tests for bulkDisconnect, bulkReboot, and bulkSetWiFi
*/
import { MockBLEManager } from '../MockBLEManager';
import { BLEConnectionState } from '../types';
describe('BLEManager Bulk Operations', () => {
let manager: MockBLEManager;
beforeEach(() => {
manager = new MockBLEManager();
});
afterEach(async () => {
await manager.cleanup();
});
describe('bulkDisconnect', () => {
it('should disconnect multiple devices', async () => {
// Connect devices first
await manager.connectDevice('mock-743');
await manager.connectDevice('mock-769');
expect(manager.isDeviceConnected('mock-743')).toBe(true);
expect(manager.isDeviceConnected('mock-769')).toBe(true);
// Bulk disconnect
const results = await manager.bulkDisconnect(['mock-743', 'mock-769']);
expect(results).toHaveLength(2);
expect(results[0].success).toBe(true);
expect(results[1].success).toBe(true);
expect(manager.isDeviceConnected('mock-743')).toBe(false);
expect(manager.isDeviceConnected('mock-769')).toBe(false);
});
it('should return results with device info', async () => {
await manager.connectDevice('mock-743');
const results = await manager.bulkDisconnect(['mock-743']);
expect(results[0]).toEqual({
deviceId: 'mock-743',
deviceName: 'WP_497_81a14c',
success: true,
});
});
it('should handle empty device list', async () => {
const results = await manager.bulkDisconnect([]);
expect(results).toEqual([]);
});
it('should handle non-connected devices gracefully', async () => {
const results = await manager.bulkDisconnect(['non-existent-device']);
expect(results).toHaveLength(1);
expect(results[0].success).toBe(true); // Mock doesn't throw for non-connected
expect(results[0].deviceId).toBe('non-existent-device');
});
it('should continue processing after individual failure', async () => {
await manager.connectDevice('mock-743');
await manager.connectDevice('mock-769');
// Disconnect all
const results = await manager.bulkDisconnect(['mock-743', 'mock-769']);
// All should have been processed
expect(results).toHaveLength(2);
});
});
describe('bulkReboot', () => {
it('should reboot multiple devices', async () => {
await manager.connectDevice('mock-743');
await manager.connectDevice('mock-769');
const results = await manager.bulkReboot(['mock-743', 'mock-769']);
expect(results).toHaveLength(2);
expect(results[0].success).toBe(true);
expect(results[1].success).toBe(true);
});
it('should return results with device names', async () => {
await manager.connectDevice('mock-743');
const results = await manager.bulkReboot(['mock-743']);
expect(results[0].deviceId).toBe('mock-743');
expect(results[0].deviceName).toBe('WP_497_81a14c');
expect(results[0].success).toBe(true);
});
it('should disconnect devices after reboot', async () => {
await manager.connectDevice('mock-743');
expect(manager.isDeviceConnected('mock-743')).toBe(true);
await manager.bulkReboot(['mock-743']);
expect(manager.isDeviceConnected('mock-743')).toBe(false);
});
it('should handle empty device list', async () => {
const results = await manager.bulkReboot([]);
expect(results).toEqual([]);
});
});
describe('bulkSetWiFi', () => {
const devices = [
{ id: 'mock-743', name: 'WP_497_81a14c' },
{ id: 'mock-769', name: 'WP_523_81aad4' },
];
const ssid = 'TestNetwork';
const password = 'testpass123';
it('should configure WiFi on multiple devices', async () => {
const results = await manager.bulkSetWiFi(devices, ssid, password);
expect(results).toHaveLength(2);
expect(results[0].success).toBe(true);
expect(results[1].success).toBe(true);
});
it('should return detailed results', async () => {
const results = await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password);
expect(results[0]).toEqual({
deviceId: 'mock-743',
deviceName: 'WP_497_81a14c',
success: true,
});
});
it('should call progress callback for each stage', async () => {
const progressCalls: Array<{
deviceId: string;
status: string;
error?: string;
}> = [];
const onProgress = (
deviceId: string,
status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error',
error?: string
) => {
progressCalls.push({ deviceId, status, error });
};
await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password, onProgress);
// Should have: connecting, configuring, rebooting, success
expect(progressCalls).toHaveLength(4);
expect(progressCalls[0].status).toBe('connecting');
expect(progressCalls[1].status).toBe('configuring');
expect(progressCalls[2].status).toBe('rebooting');
expect(progressCalls[3].status).toBe('success');
});
it('should handle empty device list', async () => {
const results = await manager.bulkSetWiFi([], ssid, password);
expect(results).toEqual([]);
});
it('should handle wrong password', async () => {
const results = await manager.bulkSetWiFi(
[{ id: 'mock-743', name: 'WP_497' }],
ssid,
'wrongpass' // Mock treats 'wrongpass' as incorrect
);
expect(results[0].success).toBe(false);
expect(results[0].error).toBeDefined();
});
it('should call error progress callback on failure', async () => {
const progressCalls: string[] = [];
const onProgress = (
_deviceId: string,
status: string,
_error?: string
) => {
progressCalls.push(status);
};
await manager.bulkSetWiFi(
[{ id: 'mock-743', name: 'WP_497' }],
ssid,
'wrongpass',
onProgress
);
expect(progressCalls).toContain('error');
});
it('should continue after one device fails', async () => {
// First device will fail (wrong password), second should still process
const mixedDevices = [
{ id: 'fail-device', name: 'FailDevice' },
{ id: 'mock-769', name: 'WP_523_81aad4' },
];
const results = await manager.bulkSetWiFi(
mixedDevices,
'OfflineNetwork', // Mock treats 'offline' SSIDs as failures
password
);
expect(results).toHaveLength(2);
// Both should have been processed (not short-circuited)
});
it('should validate WiFi credentials', async () => {
// Invalid SSID with pipe character
const results = await manager.bulkSetWiFi(
[{ id: 'mock-743', name: 'WP_497' }],
'Invalid|SSID',
password
);
expect(results[0].success).toBe(false);
expect(results[0].error).toContain('invalid character');
});
it('should validate password length', async () => {
// Password too short (< 8 chars)
const results = await manager.bulkSetWiFi(
[{ id: 'mock-743', name: 'WP_497' }],
ssid,
'short'
);
expect(results[0].success).toBe(false);
// Mock returns generic invalid credentials error for short password
expect(results[0].error).toContain('invalid');
});
it('should reboot devices after successful WiFi config', async () => {
await manager.connectDevice('mock-743');
expect(manager.isDeviceConnected('mock-743')).toBe(true);
await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password);
// Device should be disconnected after reboot
expect(manager.isDeviceConnected('mock-743')).toBe(false);
});
});
describe('Bulk Operations - Concurrency', () => {
it('should handle parallel bulk operations', async () => {
// Connect devices
await Promise.all([
manager.connectDevice('mock-743'),
manager.connectDevice('mock-769'),
]);
// Run bulk disconnect
const disconnectResults = await manager.bulkDisconnect(['mock-743', 'mock-769']);
expect(disconnectResults.every(r => r.success)).toBe(true);
});
it('should track all results independently', async () => {
const devices = [
{ id: 'mock-743', name: 'WP_497' },
{ id: 'mock-769', name: 'WP_523' },
];
const results = await manager.bulkSetWiFi(devices, 'Network', 'password123');
// Each device should have its own result
const device1Result = results.find(r => r.deviceId === 'mock-743');
const device2Result = results.find(r => r.deviceId === 'mock-769');
expect(device1Result).toBeDefined();
expect(device2Result).toBeDefined();
expect(device1Result?.deviceId).not.toBe(device2Result?.deviceId);
});
});
describe('Bulk Operations - Edge Cases', () => {
it('should handle duplicate device IDs', async () => {
await manager.connectDevice('mock-743');
const results = await manager.bulkDisconnect(['mock-743', 'mock-743']);
// Both should be processed (first disconnects, second is already disconnected)
expect(results).toHaveLength(2);
});
it('should handle mixed valid and invalid devices', async () => {
const devices = [
{ id: 'mock-743', name: 'WP_497' },
{ id: 'invalid-device', name: 'Invalid' },
];
const results = await manager.bulkSetWiFi(
devices,
'TestNetwork',
'password123'
);
expect(results).toHaveLength(2);
});
it('should handle multiple device lists', async () => {
// Reduced from 10 to 3 devices to avoid timeout
const manyDevices = Array.from({ length: 3 }, (_, i) => ({
id: `device-${i}`,
name: `WP_${i}`,
}));
const results = await manager.bulkSetWiFi(
manyDevices,
'Network',
'password123'
);
expect(results).toHaveLength(3);
}, 30000);
});
});