matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
206 lines • 10.5 kB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LOCAL_REFRESH_INTERVAL_MULTIPLIER, MQTT_REFRESH_INTERVAL_MULTIPLIER } from '../../constants/index.js';
import { PollingService } from '../../services/pollingService.js';
import { asPartial, asType, createMockLogger } from '../helpers/testUtils.js';
describe('PollingService', () => {
let service;
let mockLogger;
let mockMessageRoutingService;
let mockMessageDispatcher;
const TEST_REFRESH_INTERVAL = 100;
const mockDevice = {
duid: 'test-duid-123',
name: 'Test Vacuum',
localKey: 'test-local-key',
};
const mockDevice2 = {
duid: 'test-duid-456',
name: 'Test Vacuum 2',
localKey: 'test-local-key-2',
};
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
mockLogger = createMockLogger();
mockMessageDispatcher = asPartial({
getDeviceStatus: vi.fn(async (_duid) => undefined),
startRoomCleaning: vi.fn(),
pauseCleaning: vi.fn(),
resumeCleaning: vi.fn(),
stopCleaning: vi.fn(),
goHome: vi.fn(),
findMyRobot: vi.fn(),
getRoomMap: vi.fn(),
sendCustomMessage: vi.fn(),
getCustomMessage: vi.fn(),
getCleanModeData: vi.fn(),
changeCleanMode: vi.fn(),
});
mockMessageRoutingService = asPartial({
getMessageDispatcher: vi.fn().mockReturnValue(mockMessageDispatcher),
getCleanModeData: vi.fn(),
getRoomIdFromMap: vi.fn(),
changeCleanMode: vi.fn(),
startClean: vi.fn(),
pauseClean: vi.fn(),
stopAndGoHome: vi.fn(),
resumeClean: vi.fn(),
playSoundToLocate: vi.fn(),
customGet: vi.fn(),
customSend: vi.fn(),
setIotApi: vi.fn(),
clearAll: vi.fn(),
});
service = new PollingService(TEST_REFRESH_INTERVAL, mockLogger, mockMessageRoutingService);
});
afterEach(() => {
service.stopPolling();
vi.useRealTimers();
});
describe('activateDeviceNotifyOverLocal', () => {
it('should activate polling and start interval', () => {
service.activateDeviceNotifyOverLocal(mockDevice);
expect(mockLogger.debug).toHaveBeenCalledWith('Activating device status polling for:', mockDevice.duid);
expect(service['localIntervals'].has(mockDevice.duid)).toBe(true);
});
it('should clear existing interval before creating new one for the same device', () => {
service.activateDeviceNotifyOverLocal(mockDevice);
const firstInterval = service['localIntervals'].get(mockDevice.duid);
service.activateDeviceNotifyOverLocal(mockDevice);
const secondInterval = service['localIntervals'].get(mockDevice.duid);
expect(firstInterval).not.toBe(secondInterval);
});
it('should handle multiple polling cycles', async () => {
const mockMessageData = { state: 8, error_code: 0, dock_error_status: 0, dock_type: 1 };
const mockResponse = {
getMessage: () => mockMessageData,
};
mockMessageDispatcher.getDeviceStatus = vi.fn().mockResolvedValue(mockResponse);
service.activateDeviceNotifyOverLocal(mockDevice);
for (let i = 0; i < 3; i++) {
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER);
}
expect(mockMessageDispatcher.getDeviceStatus).toHaveBeenCalledTimes(3);
});
it('should handle getDeviceStatus errors gracefully', async () => {
const testError = new Error('Network timeout');
mockMessageDispatcher.getDeviceStatus = vi.fn().mockRejectedValue(testError);
service.activateDeviceNotifyOverLocal(mockDevice);
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER);
expect(mockLogger.error).toHaveBeenCalledWith('Failed to get device status:', testError);
});
});
describe('requestStatusOnce', () => {
it('should call getDeviceStatus for the given duid', async () => {
await service.requestStatusOnce(mockDevice.duid);
expect(mockMessageDispatcher.getDeviceStatus).toHaveBeenCalledWith(mockDevice.duid);
});
it('should return early when no dispatcher is found', async () => {
vi.mocked(mockMessageRoutingService.getMessageDispatcher).mockReturnValue(asType(undefined));
await service.requestStatusOnce(mockDevice.duid);
expect(mockMessageDispatcher.getDeviceStatus).not.toHaveBeenCalled();
});
it('should log error and not throw when getDeviceStatus fails', async () => {
const testError = new Error('Network timeout');
vi.mocked(mockMessageDispatcher.getDeviceStatus).mockRejectedValue(testError);
await expect(service.requestStatusOnce(mockDevice.duid)).resolves.not.toThrow();
expect(mockLogger.error).toHaveBeenCalledWith('Failed to get device status:', testError);
});
it('should not affect the recurring polling interval', async () => {
service.activateDeviceNotifyOverLocal(mockDevice);
const intervalBefore = service['localIntervals'].get(mockDevice.duid);
await service.requestStatusOnce(mockDevice.duid);
expect(service['localIntervals'].get(mockDevice.duid)).toBe(intervalBefore);
});
});
describe('stopPolling', () => {
it('should clear the polling interval', () => {
service.activateDeviceNotifyOverLocal(mockDevice);
expect(service['localIntervals'].has(mockDevice.duid)).toBe(true);
service.stopPolling();
expect(service['localIntervals'].size).toBe(0);
});
it('should handle calling stopPolling when no interval is active', () => {
expect(() => {
service.stopPolling();
}).not.toThrow();
expect(service['localIntervals'].size).toBe(0);
});
it('should prevent further polling after stopPolling is called', async () => {
mockMessageDispatcher.getDeviceStatus = vi.fn().mockResolvedValue({
errorStatus: { error: 0 },
message: { state: 8 },
});
service.activateDeviceNotifyOverLocal(mockDevice);
service.stopPolling();
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER * 3);
expect(mockMessageDispatcher.getDeviceStatus).not.toHaveBeenCalled();
});
});
describe('shutdown', () => {
it('should stop polling and clear deviceNotify callback', async () => {
service.activateDeviceNotifyOverLocal(mockDevice);
await service.shutdown();
expect(service['localIntervals'].size).toBe(0);
});
it('should handle shutdown when nothing is active', async () => {
await expect(service.shutdown()).resolves.not.toThrow();
});
it('should prevent polling after shutdown', async () => {
mockMessageDispatcher.getDeviceStatus = vi.fn().mockResolvedValue({
errorStatus: { error: 0 },
message: { state: 8 },
});
service.activateDeviceNotifyOverLocal(mockDevice);
await service.shutdown();
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER * 3);
expect(mockMessageDispatcher.getDeviceStatus).not.toHaveBeenCalled();
});
});
describe('multi-device polling', () => {
it('should not stop device1 polling when device2 is activated', async () => {
service.activateDeviceNotifyOverLocal(mockDevice);
service.activateDeviceNotifyOverLocal(mockDevice2);
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER);
const calls = vi.mocked(mockMessageDispatcher.getDeviceStatus).mock.calls;
const device1Calls = calls.filter(([duid]) => duid === mockDevice.duid);
expect(device1Calls.length).toBeGreaterThan(0);
});
it('should poll both devices independently when two devices are activated', async () => {
service.activateDeviceNotifyOverLocal(mockDevice);
service.activateDeviceNotifyOverLocal(mockDevice2);
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER);
const calls = vi.mocked(mockMessageDispatcher.getDeviceStatus).mock.calls;
const device1Calls = calls.filter(([duid]) => duid === mockDevice.duid);
const device2Calls = calls.filter(([duid]) => duid === mockDevice2.duid);
expect(device1Calls.length).toBeGreaterThan(0);
expect(device2Calls.length).toBeGreaterThan(0);
});
});
describe('integration scenarios', () => {
it('should switch from local to MQTT polling', async () => {
const localResponse = {
errorStatus: { error: 0 },
message: { state: 8 },
};
const mqttResponse = {
errorStatus: { error: 0 },
message: { state: 5 },
};
mockMessageDispatcher.getDeviceStatus = vi.fn().mockResolvedValue(localResponse);
service.activateDeviceNotifyOverLocal(mockDevice);
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * LOCAL_REFRESH_INTERVAL_MULTIPLIER);
expect(mockMessageDispatcher.getDeviceStatus).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(TEST_REFRESH_INTERVAL * MQTT_REFRESH_INTERVAL_MULTIPLIER);
expect(mockMessageDispatcher.getDeviceStatus).toHaveBeenCalledTimes(1);
});
it('should handle rapid activation/deactivation cycles', () => {
for (let i = 0; i < 5; i++) {
service.activateDeviceNotifyOverLocal(mockDevice);
service.stopPolling();
}
expect(service['localIntervals'].size).toBe(0);
});
});
});
//# sourceMappingURL=pollingService.test.js.map