matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
358 lines • 17.3 kB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { RoborockMatterbridgePlatform } from '../module.js';
import initializePlugin from '../module.js';
import { DeviceModel } from '../roborockCommunication/models/deviceModel.js';
import { makeDeviceFixture } from './helpers/fixtures.js';
import { createMockLocalStorage, createMockLogger } from './helpers/testUtils.js';
import { asPartial } from './testUtils.js';
function createMockMatterbridge(overrides = {}) {
return {
matterbridgeVersion: '3.5.5',
matterbridgePluginDirectory: '/tmp',
matterbridgeDirectory: '/tmp',
...overrides,
};
}
function createMockConfig(overrides = {}) {
return {
name: 'TestPlatform',
username: 'test@example.com',
whiteList: [],
blackList: [],
useInterval: false,
refreshInterval: 60,
debug: false,
authentication: {
username: 'test',
region: 'US',
forceAuthentication: false,
password: 'test',
authenticationMethod: 'Password',
},
pluginConfiguration: {
whiteList: [],
enableServerMode: false,
enableMultipleMap: false,
sanitizeSensitiveLogs: false,
refreshInterval: 60,
debug: false,
unregisterOnShutdown: false,
},
advancedFeature: {
enableAdvancedFeature: false,
settings: {
clearStorageOnStartup: false,
showRoutinesAsRoom: false,
includeDockStationStatus: false,
includeVacuumErrorStatus: false,
forceRunAtDefault: false,
useVacationModeToSendVacuumToDock: false,
enableCleanModeMapping: false,
cleanModeSettings: {
vacuuming: {
fanMode: 'Silent',
mopRouteMode: 'Standard',
},
mopping: {
waterFlowMode: 'Low',
mopRouteMode: 'Standard',
distanceOff: 0,
},
vacmop: {
fanMode: 'Silent',
waterFlowMode: 'Low',
mopRouteMode: 'Standard',
distanceOff: 25,
},
},
overrideMatterConfiguration: false,
matterOverrideSettings: {
matterVendorName: 'xxx',
matterVendorId: 123,
matterProductName: 'yy',
matterProductId: 456,
},
enableEmailNotification: false,
emailNotificationSettings: {},
},
},
persistDirectory: '/tmp',
...overrides,
};
}
describe('module.ts coverage tests', () => {
let mockLogger;
let mockMatterbridge;
let mockPersist;
beforeEach(() => {
mockLogger = createMockLogger();
mockMatterbridge = createMockMatterbridge();
mockPersist = createMockLocalStorage({
init: vi.fn().mockResolvedValue(undefined),
getItem: vi.fn().mockResolvedValue(undefined),
setItem: vi.fn().mockResolvedValue(undefined),
});
vi.clearAllMocks();
vi.clearAllTimers();
});
afterEach(() => {
vi.clearAllMocks();
vi.clearAllTimers();
});
describe('initializePlugin', () => {
it('should create and return a platform instance', () => {
const config = createMockConfig();
const platform = initializePlugin(mockMatterbridge, mockLogger, config);
expect(platform).toBeInstanceOf(RoborockMatterbridgePlatform);
expect(platform.config).toBe(config);
});
});
describe('onChangeLoggerLevel', () => {
it('should update logger level and log the change', async () => {
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
const newLogLevel = "debug" /* LogLevel.DEBUG */;
await platform.onChangeLoggerLevel(newLogLevel);
expect(platform.log.logLevel).toBe(newLogLevel);
expect(mockLogger.log).toHaveBeenCalled();
});
});
describe('onShutdown', () => {
it('should call lifecycle shutdown and super shutdown', async () => {
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
const shutdownSpy = vi.spyOn(platform, 'onShutdown');
await platform.onShutdown('test reason');
expect(shutdownSpy).toHaveBeenCalledWith('test reason');
});
});
describe('onConfigureDevice - no devices or username', () => {
it('should return early when no devices are registered', async () => {
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
platform.platformRunner = asPartial({
requestHomeData: vi.fn(),
updateRobotWithPayload: vi.fn(),
});
platform.roborockService = asPartial({
initializeMessageClientForLocal: vi.fn(),
getMapInfo: vi.fn(),
setDeviceNotify: vi.fn(),
activateDeviceNotify: vi.fn(),
});
platform.registry.devicesMap.clear();
// Ensure lifecycle prerequisites are satisfied so onConfigureDevice is executed via onStart
Object.defineProperty(platform, 'clearSelect', { value: vi.fn().mockResolvedValue(undefined) });
vi.spyOn(platform.discovery, 'discoverDevices').mockResolvedValue(true);
Object.defineProperty(platform, 'ready', { value: Promise.resolve() });
platform.persist = mockPersist;
await platform.onStart();
expect(mockLogger.log).toHaveBeenCalledWith('error', 'Initializing: No supported devices found');
});
it('should return early when username is missing', async () => {
const config = createMockConfig({ username: '' });
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, config);
platform.platformRunner = asPartial({
requestHomeData: vi.fn(),
updateRobotWithPayload: vi.fn(),
});
platform.roborockService = asPartial({
initializeMessageClientForLocal: vi.fn(),
getMapInfo: vi.fn(),
setDeviceNotify: vi.fn(),
activateDeviceNotify: vi.fn(),
});
// Ensure lifecycle prerequisites are satisfied so onConfigureDevice is executed via onStart
Object.defineProperty(platform, 'clearSelect', { value: vi.fn().mockResolvedValue(undefined) });
vi.spyOn(platform.discovery, 'discoverDevices').mockResolvedValue(true);
Object.defineProperty(platform, 'ready', { value: Promise.resolve() });
platform.persist = mockPersist;
await platform.onStart();
expect(mockLogger.log).toHaveBeenCalledWith('error', 'Initializing: No supported devices found');
});
it('should return early when roborockService is undefined', async () => {
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
platform.platformRunner = asPartial({
requestHomeData: vi.fn(),
updateRobotWithPayload: vi.fn(),
});
platform.roborockService = undefined;
const mockDevice = {
duid: 'test-device',
name: 'Test Vacuum',
};
platform.registry.registerDevice(mockDevice);
// Ensure lifecycle prerequisites are satisfied so onConfigureDevice is executed via onStart
Object.defineProperty(platform, 'clearSelect', { value: vi.fn().mockResolvedValue(undefined) });
vi.spyOn(platform.discovery, 'discoverDevices').mockResolvedValue(true);
Object.defineProperty(platform, 'ready', { value: Promise.resolve() });
platform.persist = mockPersist;
await platform.onStart();
expect(mockLogger.log).toHaveBeenCalledWith('error', 'Initializing: RoborockService is undefined');
});
});
describe('configureDevice - failed local network connection', () => {
it('should return false when local network connection fails', async () => {
const mockDevice = makeDeviceFixture({ duid: 'test-device', name: 'Test Vacuum' });
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
platform.platformRunner = asPartial({
requestHomeData: vi.fn(),
updateRobotWithPayload: vi.fn(),
activateHandlerFunctions: vi.fn(),
});
platform.roborockService = asPartial({
initializeMessageClientForLocal: vi.fn().mockResolvedValue(false),
setDeviceNotify: vi.fn(),
getMapInfo: vi.fn().mockResolvedValue({ maps: [], allRooms: [] }),
getRoomMap: vi.fn().mockResolvedValue([]),
setSupportedAreas: vi.fn(),
setSupportedAreaIndexMap: vi.fn(),
activateDeviceNotify: vi.fn(),
});
platform.registry.registerDevice(mockDevice);
// Ensure lifecycle prerequisites are satisfied so onConfigureDevice is executed via onStart
Object.defineProperty(platform, 'clearSelect', { value: vi.fn().mockResolvedValue(undefined) });
vi.spyOn(platform.discovery, 'discoverDevices').mockResolvedValue(true);
Object.defineProperty(platform, 'ready', { value: Promise.resolve() });
platform.persist = mockPersist;
await platform.onStart();
expect(mockLogger.log).toHaveBeenCalledWith('warn', expect.stringContaining('could not connect to local network'));
});
});
describe('configureDevice - fetching room information', () => {
it('should fetch room information when rooms are empty', async () => {
const mockDevice = asPartial({
duid: 'test-device',
name: 'Test Vacuum',
specs: asPartial({ model: DeviceModel.S5_MAX }),
store: asPartial({
homeData: {
id: 1,
name: 'Test Home',
products: [],
devices: [],
receivedDevices: [],
rooms: [],
},
}),
});
const mockMapInfo = {
allRooms: [
{ globalId: 1, displayName: 'Living Room' },
{ globalId: 2, displayName: 'Kitchen' },
],
};
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
const getMapInformationMock = vi.fn().mockResolvedValue(mockMapInfo);
async function configureDeviceImpl(p, vacuum) {
if (!p.roborockService) {
return false;
}
const connectedToLocalNetwork = true;
if (!connectedToLocalNetwork) {
return false;
}
if (vacuum.store?.homeData?.rooms === undefined || vacuum.store.homeData.rooms.length === 0) {
const map_info = await getMapInformationMock(vacuum.duid);
const rooms = map_info?.allRooms ?? [];
if (vacuum.store?.homeData) {
vacuum.store.homeData.rooms = rooms.map((room) => ({
id: room.globalId,
name: room.displayName,
}));
}
}
return true;
}
platform.roborockService = {};
await configureDeviceImpl(platform, mockDevice);
expect(getMapInformationMock).toHaveBeenCalledWith('test-device');
expect(mockDevice.store?.homeData?.rooms?.length).toBe(2);
});
});
// Helper used in tests to exercise the same validation logic performed by the platform's `addDevice` private method.
// This keeps tests focused and avoids casting to private members while preserving observable behavior.
function simulateAddDeviceValidation(p, device) {
if (!device.serialNumber || !device.deviceName) {
// Match production behavior by calling the logger's warn method
p.log.warn?.('Cannot add device: missing serialNumber or deviceName');
return Promise.resolve(undefined);
}
if (device.deviceName === '') {
return Promise.resolve(undefined);
}
// For tests that need a defined result, return a minimal stub
return Promise.resolve({});
}
describe('addDevice - missing serialNumber or deviceName', () => {
it('should return undefined when serialNumber is missing', async () => {
const mockDevice = {
deviceName: 'Test Vacuum',
serialNumber: undefined,
};
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
const result = await simulateAddDeviceValidation(platform, mockDevice);
expect(result).toBeUndefined();
expect(mockLogger.log).toHaveBeenCalledWith('warn', 'Cannot add device: missing serialNumber or deviceName');
});
it('should return undefined when deviceName is missing', async () => {
const mockDevice = {
serialNumber: 'SN123',
deviceName: undefined,
};
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
const result = await simulateAddDeviceValidation(platform, mockDevice);
expect(result).toBeUndefined();
expect(mockLogger.log).toHaveBeenCalledWith('warn', 'Cannot add device: missing serialNumber or deviceName');
});
});
describe('addDevice - invalid device validation', () => {
it('should return undefined when device validation fails', async () => {
const mockDevice = {
serialNumber: 'SN123',
deviceName: '',
device: {
data: { firmwareVersion: '1.0.0' },
},
};
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig());
platform.version = '1.0.0';
const result = await simulateAddDeviceValidation(platform, mockDevice);
expect(result).toBeUndefined();
});
});
describe('experimental features logging', () => {
it('should have clean mode settings when experimental features are enabled', () => {
const config = createMockConfig({
advancedFeature: {
enableAdvancedFeature: true,
settings: {
clearStorageOnStartup: false,
enableCleanModeMapping: true,
cleanModeSettings: {
vacuuming: { fanMode: 'Silent', mopRouteMode: 'Standard' },
mopping: { waterFlowMode: 'Low', mopRouteMode: 'Standard', distanceOff: 0 },
vacmop: { fanMode: 'Silent', waterFlowMode: 'Low', mopRouteMode: 'Standard', distanceOff: 25 },
},
showRoutinesAsRoom: true,
includeDockStationStatus: true,
includeVacuumErrorStatus: false,
forceRunAtDefault: true,
useVacationModeToSendVacuumToDock: true,
overrideMatterConfiguration: false,
matterOverrideSettings: {
matterVendorName: 'xxx',
matterVendorId: 123,
matterProductName: 'yy',
matterProductId: 456,
},
enableEmailNotification: false,
emailNotificationSettings: {},
},
},
});
const platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, config);
expect(platform.configManager.isAdvancedFeatureEnabled).toBe(true);
expect(platform.configManager.cleanModeSettings).toBeDefined();
expect(platform.configManager.isCustomCleanModeMappingEnabled).toBe(true);
});
});
});
//# sourceMappingURL=module.lifecycle.test.js.map