feat: Use Vitest for testing (#25284)

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
This commit is contained in:
Nerivec
2024-12-22 20:19:45 +01:00
committed by GitHub
co-authored by Koen Kanters
parent 5d059b7bcb
commit 06f44533af
51 changed files with 2606 additions and 4771 deletions
+153 -146
View File
@@ -1,9 +1,18 @@
import * as data from './mocks/data';
import {mockLogger} from './mocks/logger';
import {mockMQTT, mockMQTTConnectAsync, events as mockMQTTEvents} from './mocks/mqtt';
import {flushPromises, JestMockAny} from './mocks/utils';
import {
mockMQTTConnectAsync,
mockMQTTEndAsync,
events as mockMQTTEvents,
mockMQTTPublishAsync,
mockMQTTSubscribeAsync,
mockMQTTUnsubscribeAsync,
} from './mocks/mqtt';
import {flushPromises} from './mocks/utils';
import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from './mocks/zigbeeHerdsman';
import type {Mock, MockInstance} from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
@@ -18,27 +27,21 @@ import * as settings from '../lib/util/settings';
process.env.NOTIFY_SOCKET = 'mocked';
const LOG_MQTT_NS = 'z2m:mqtt';
jest.mock(
'sd-notify',
() => {
return {
watchdogInterval: jest.fn(() => 3000),
startWatchdogMode: jest.fn(),
stopWatchdogMode: jest.fn(),
ready: jest.fn(),
stopping: jest.fn(),
};
},
{virtual: true},
);
vi.mock('sd-notify', () => ({
watchdogInterval: vi.fn(() => 3000),
startWatchdogMode: vi.fn(),
stopWatchdogMode: vi.fn(),
ready: vi.fn(),
stopping: vi.fn(),
}));
const mocksClear = [
mockZHController.stop,
mockMQTT.endAsync,
mockMQTT.publishAsync,
mockMQTT.subscribeAsync,
mockMQTT.unsubscribeAsync,
mockMQTT.endAsync,
mockMQTTEndAsync,
mockMQTTPublishAsync,
mockMQTTSubscribeAsync,
mockMQTTUnsubscribeAsync,
mockMQTTEndAsync,
mockMQTTConnectAsync,
devices.bulb_color.removeFromNetwork,
devices.bulb.removeFromNetwork,
@@ -50,25 +53,25 @@ const mocksClear = [
describe('Controller', () => {
let controller: Controller;
let mockExit: JestMockAny;
let mockExit: Mock;
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
});
beforeEach(() => {
returnDevices.splice(0);
mockExit = jest.fn();
mockExit = vi.fn();
data.writeDefaultConfiguration();
settings.reRead();
controller = new Controller(jest.fn(), mockExit);
controller = new Controller(vi.fn(), mockExit);
mocksClear.forEach((m) => m.mockClear());
settings.reRead();
data.writeDefaultState();
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
afterEach(async () => {
@@ -104,12 +107,12 @@ describe('Controller', () => {
will: {payload: Buffer.from('{"state":"offline"}'), retain: true, topic: 'zigbee2mqtt/bridge/state', qos: 1},
properties: {maximumPacketSize: 1048576},
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
{retain: true, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {retain: true, qos: 0});
});
it('Start controller with specific MQTT settings', async () => {
@@ -160,9 +163,9 @@ describe('Controller', () => {
settings.set(['advanced', 'ext_pan_id'], 'GENERATE');
await controller.start();
await flushPromises();
expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.networkKey.length).toStrictEqual(16);
expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.extendedPanID.length).toStrictEqual(8);
expect((ZHController as unknown as jest.Mock).mock.calls[0][0].network.panID).toStrictEqual(expect.any(Number));
expect((ZHController as unknown as MockInstance).mock.calls[0][0].network.networkKey.length).toStrictEqual(16);
expect((ZHController as unknown as MockInstance).mock.calls[0][0].network.extendedPanID.length).toStrictEqual(8);
expect((ZHController as unknown as MockInstance).mock.calls[0][0].network.panID).toStrictEqual(expect.any(Number));
expect(data.read().advanced.network_key.length).toStrictEqual(16);
expect(data.read().advanced.ext_pan_id.length).toStrictEqual(8);
expect(data.read().advanced.pan_id).toStrictEqual(expect.any(Number));
@@ -172,13 +175,13 @@ describe('Controller', () => {
data.writeDefaultState();
await controller.start();
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
{qos: 0, retain: true},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {qos: 0, retain: false});
});
it('Start controller should not publish cached states when disabled', async () => {
@@ -186,7 +189,7 @@ describe('Controller', () => {
data.writeDefaultState();
await controller.start();
await flushPromises();
const publishedTopics = mockMQTT.publishAsync.mock.calls.map((m) => m[0]);
const publishedTopics = mockMQTTPublishAsync.mock.calls.map((m) => m[0]);
expect(publishedTopics).toEqual(expect.not.arrayContaining(['zigbee2mqtt/bulb', 'zigbee2mqtt/remote']));
});
@@ -195,12 +198,12 @@ describe('Controller', () => {
data.writeDefaultState();
await controller.start();
await flushPromises();
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
`{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}`,
{qos: 0, retain: true},
);
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true});
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/remote', `{"brightness":255}`, {qos: 0, retain: true});
});
it('Log when MQTT client is unavailable', async () => {
@@ -209,7 +212,7 @@ describe('Controller', () => {
mockLogger.error.mockClear();
// @ts-expect-error private
controller.mqtt.client.reconnecting = true;
jest.advanceTimersByTime(11 * 1000);
vi.advanceTimersByTime(11 * 1000);
expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!');
// @ts-expect-error private
controller.mqtt.client.reconnecting = false;
@@ -289,7 +292,7 @@ describe('Controller', () => {
it('Start controller and stop with restart', async () => {
await controller.start();
await controller.stop(true);
expect(mockMQTT.endAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTEndAsync).toHaveBeenCalledTimes(1);
expect(mockZHController.stop).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledWith(0, true);
@@ -299,7 +302,7 @@ describe('Controller', () => {
mockZHController.stop.mockRejectedValueOnce('failed');
await controller.start();
await controller.stop();
expect(mockMQTT.endAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTEndAsync).toHaveBeenCalledTimes(1);
expect(mockZHController.stop).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledWith(1, false);
@@ -310,7 +313,7 @@ describe('Controller', () => {
await controller.start();
await mockZHEvents.adapterDisconnected();
await flushPromises();
expect(mockMQTT.endAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTEndAsync).toHaveBeenCalledTimes(1);
expect(mockZHController.stop).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledWith(1, false);
@@ -324,13 +327,15 @@ describe('Controller', () => {
mockLogger.info.mockClear();
mockMQTTEvents.error(new Error('ECONNRESET'));
mockMQTT.reconnecting = true;
// @ts-expect-error private
controller.mqtt.client.reconnecting = true;
expect(mockLogger.error).toHaveBeenCalledWith('MQTT error: ECONNRESET');
await jest.advanceTimersByTimeAsync(11000);
await vi.advanceTimersByTimeAsync(11000);
expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!');
mockMQTT.reconnecting = false;
// @ts-expect-error private
controller.mqtt.client.reconnecting = false;
await mockMQTTEvents.connect();
expect(mockLogger.info).toHaveBeenCalledWith('Connected to MQTT server');
});
@@ -346,13 +351,15 @@ describe('Controller', () => {
reasonCode: 149,
properties: {reasonString: 'Maximum packet size was exceeded'},
});
mockMQTT.disconnecting = true;
// @ts-expect-error private
controller.mqtt.client.disconnecting = true;
expect(mockLogger.error).toHaveBeenCalledWith('MQTT disconnect: reason 149 (Maximum packet size was exceeded)');
await jest.advanceTimersByTimeAsync(11000);
await vi.advanceTimersByTimeAsync(11000);
expect(mockLogger.error).toHaveBeenCalledWith('Not connected to MQTT server!');
mockMQTT.disconnecting = false;
// @ts-expect-error private
controller.mqtt.client.disconnecting = false;
await mockMQTTEvents.connect();
expect(mockLogger.info).toHaveBeenCalledWith('Connected to MQTT server');
});
@@ -360,9 +367,9 @@ describe('Controller', () => {
it('Handles MQTT publish error', async () => {
await controller.start();
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// fail on device_joined (has skipLog=false)
mockMQTT.publishAsync.mockImplementationOnce(mockMQTT.publishAsync.getMockImplementation()!).mockImplementationOnce(() => {
mockMQTTPublishAsync.mockImplementationOnce(mockMQTTPublishAsync.getMockImplementation()!).mockImplementationOnce(() => {
throw new Error('client disconnecting');
});
await mockZHEvents.deviceJoined({device: devices.bulb});
@@ -373,7 +380,7 @@ describe('Controller', () => {
it('Handle mqtt message', async () => {
// @ts-expect-error private
const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation();
const spyEventbusEmitMQTTMessage = vi.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(vi.fn());
await controller.start();
mockLogger.debug.mockClear();
@@ -384,7 +391,7 @@ describe('Controller', () => {
it('Skip MQTT messages on topic we published to', async () => {
// @ts-expect-error private
const spyEventbusEmitMQTTMessage = jest.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation();
const spyEventbusEmitMQTTMessage = vi.spyOn(controller.eventBus, 'emitMQTTMessage').mockImplementation(vi.fn());
await controller.start();
mockLogger.debug.mockClear();
@@ -450,7 +457,7 @@ describe('Controller', () => {
const payload = {device};
await mockZHEvents.deviceJoined(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
@@ -461,7 +468,7 @@ describe('Controller', () => {
await controller.start();
const device = devices.bulb;
settings.set(['blocklist'], [device.ieeeAddr]);
const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler;
const handler = (ZHController as unknown as MockInstance).mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(false);
});
@@ -469,7 +476,7 @@ describe('Controller', () => {
await controller.start();
const device = devices.bulb;
settings.set(['blocklist'], ['123']);
const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler;
const handler = (ZHController as unknown as MockInstance).mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
@@ -477,7 +484,7 @@ describe('Controller', () => {
await controller.start();
const device = devices.bulb;
settings.set(['passlist'], [device.ieeeAddr]);
const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler;
const handler = (ZHController as unknown as MockInstance).mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
@@ -485,7 +492,7 @@ describe('Controller', () => {
await controller.start();
const device = devices.bulb;
settings.set(['passlist'], ['123']);
const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler;
const handler = (ZHController as unknown as MockInstance).mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(false);
});
@@ -494,14 +501,14 @@ describe('Controller', () => {
const device = devices.bulb;
settings.set(['passlist'], [device.ieeeAddr]);
settings.set(['blocklist'], [device.ieeeAddr]);
const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler;
const handler = (ZHController as unknown as MockInstance).mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
it('acceptJoiningDeviceHandler accept when not on blocklist and passlist', async () => {
await controller.start();
const device = devices.bulb;
const handler = (ZHController as unknown as jest.Mock).mock.calls[0][0].acceptJoiningDeviceHandler;
const handler = (ZHController as unknown as MockInstance).mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
@@ -512,7 +519,7 @@ describe('Controller', () => {
mockZHEvents.deviceJoined(payload);
mockZHEvents.deviceJoined(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_joined', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
@@ -525,7 +532,7 @@ describe('Controller', () => {
const payload = {device, status: 'started'};
await mockZHEvents.deviceInterview(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'started', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
@@ -538,7 +545,7 @@ describe('Controller', () => {
const payload = {device, status: 'failed'};
await mockZHEvents.deviceInterview(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_interview', data: {friendly_name: 'bulb', status: 'failed', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
@@ -547,13 +554,13 @@ describe('Controller', () => {
it('On zigbee deviceInterview successful supported', async () => {
await controller.start();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const device = devices.bulb;
const payload = {device, status: 'successful'};
await mockZHEvents.deviceInterview(payload);
await flushPromises();
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event');
const parsedMessage = JSON.parse(mockMQTT.publishAsync.mock.calls[1][1]);
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event');
const parsedMessage = JSON.parse(mockMQTTPublishAsync.mock.calls[1][1]);
expect(parsedMessage.type).toStrictEqual('device_interview');
expect(parsedMessage.data.friendly_name).toStrictEqual('bulb');
expect(parsedMessage.data.status).toStrictEqual('successful');
@@ -564,18 +571,18 @@ describe('Controller', () => {
expect(parsedMessage.data.definition.description).toStrictEqual('TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm');
expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array));
expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array));
expect(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
});
it('On zigbee deviceInterview successful not supported', async () => {
await controller.start();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const device = devices.unsupported;
const payload = {device, status: 'successful'};
await mockZHEvents.deviceInterview(payload);
await flushPromises();
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event');
const parsedMessage = JSON.parse(mockMQTT.publishAsync.mock.calls[1][1]);
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/event');
const parsedMessage = JSON.parse(mockMQTTPublishAsync.mock.calls[1][1]);
expect(parsedMessage.type).toStrictEqual('device_interview');
expect(parsedMessage.data.friendly_name).toStrictEqual(device.ieeeAddr);
expect(parsedMessage.data.status).toStrictEqual('successful');
@@ -586,7 +593,7 @@ describe('Controller', () => {
expect(parsedMessage.data.definition.description).toStrictEqual('Automatically generated definition');
expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array));
expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array));
expect(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
});
it('On zigbee event device announce', async () => {
@@ -596,7 +603,7 @@ describe('Controller', () => {
await mockZHEvents.deviceAnnounce(payload);
await flushPromises();
expect(mockLogger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_announce', data: {friendly_name: 'bulb', ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
@@ -607,12 +614,12 @@ describe('Controller', () => {
await controller.start();
returnDevices.push('0x00124b00120144ae');
settings.set(['devices'], {});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const device = devices.bulb;
const payload = {ieeeAddr: device.ieeeAddr};
await mockZHEvents.deviceLeave(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}),
{retain: false, qos: 0},
@@ -623,11 +630,11 @@ describe('Controller', () => {
await controller.start();
returnDevices.push('0x00124b00120144ae');
const device = devices.bulb;
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const payload = {ieeeAddr: device.ieeeAddr};
await mockZHEvents.deviceLeave(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({type: 'device_leave', data: {ieee_address: device.ieeeAddr, friendly_name: 'bulb'}}),
{retain: false, qos: 0},
@@ -637,7 +644,7 @@ describe('Controller', () => {
it('Publish entity state attribute output', async () => {
await controller.start();
settings.set(['advanced', 'output'], 'attribute');
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {
@@ -650,30 +657,30 @@ describe('Controller', () => {
brightness: 50,
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '50', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color', '100,50,10', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-1', 'yes', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/dummy-2', 'no', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/test1', '', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/test', '', {qos: 0, retain: true});
});
it('Publish entity state attribute_json output', async () => {
await controller.start();
settings.set(['advanced', 'output'], 'attribute_and_json');
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/color_temp', '370', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '99', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 99}),
{qos: 0, retain: true},
@@ -684,37 +691,37 @@ describe('Controller', () => {
await controller.start();
settings.set(['advanced', 'output'], 'attribute_and_json');
settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_attributes'], ['color_temp', 'linkquality']);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true});
});
it('Publish entity state attribute_json output filtered (device_options)', async () => {
await controller.start();
settings.set(['advanced', 'output'], 'attribute_and_json');
settings.set(['device_options', 'filtered_attributes'], ['color_temp', 'linkquality']);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON', brightness: 200, color_temp: 370, linkquality: 99});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true});
});
it('Publish entity state attribute_json output filtered cache', async () => {
await controller.start();
settings.set(['advanced', 'output'], 'attribute_and_json');
settings.set(['devices', devices.bulb.ieeeAddr, 'filtered_cache'], ['linkquality']);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
@@ -726,11 +733,11 @@ describe('Controller', () => {
// @ts-expect-error private
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}),
{qos: 0, retain: true},
@@ -741,7 +748,7 @@ describe('Controller', () => {
await controller.start();
settings.set(['advanced', 'output'], 'attribute_and_json');
settings.set(['device_options', 'filtered_cache'], ['linkquality']);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
@@ -753,11 +760,11 @@ describe('Controller', () => {
// @ts-expect-error private
expect(controller.state.state[device.ieeeAddr]).toStrictEqual({brightness: 200, color_temp: 370, state: 'ON'});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/state', 'ON', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/brightness', '200', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb/linkquality', '87', {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 200, color_temp: 370, linkquality: 87}),
{qos: 0, retain: true},
@@ -767,12 +774,12 @@ describe('Controller', () => {
it('Publish entity state with device information', async () => {
await controller.start();
settings.set(['mqtt', 'include_device_information'], true);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
let device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON'});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({
state: 'ON',
@@ -797,7 +804,7 @@ describe('Controller', () => {
device = controller.zigbee.resolveEntity('unsupported2')!;
await controller.publishEntityState(device, {state: 'ON'});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/unsupported2',
stringify({
state: 'ON',
@@ -818,12 +825,12 @@ describe('Controller', () => {
it('Should publish entity state without retain', async () => {
await controller.start();
settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], false);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON'});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
{qos: 0, retain: false},
@@ -833,12 +840,12 @@ describe('Controller', () => {
it('Should publish entity state with retain', async () => {
await controller.start();
settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON'});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
{qos: 0, retain: true},
@@ -850,12 +857,12 @@ describe('Controller', () => {
settings.set(['mqtt', 'version'], 5);
settings.set(['devices', devices.bulb.ieeeAddr, 'retain'], true);
settings.set(['devices', devices.bulb.ieeeAddr, 'retention'], 37);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON'});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({state: 'ON', brightness: 50, color_temp: 370, linkquality: 99}),
{qos: 0, retain: true, properties: {messageExpiryInterval: 37}},
@@ -865,27 +872,27 @@ describe('Controller', () => {
it('Publish entity state no empty messages', async () => {
data.writeEmptyState();
await controller.start();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Should allow to disable state persistency', async () => {
settings.set(['advanced', 'cache_state_persistent'], false);
data.removeState();
await controller.start();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON'});
await controller.publishEntityState(device, {brightness: 200});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON', brightness: 200}), {qos: 0, retain: true});
await controller.stop();
expect(data.stateExists()).toBeFalsy();
});
@@ -905,15 +912,15 @@ describe('Controller', () => {
settings.set(['advanced', 'cache_state'], false);
data.writeEmptyState();
await controller.start();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
const device = controller.zigbee.resolveEntity('bulb')!;
await controller.publishEntityState(device, {state: 'ON'});
await controller.publishEntityState(device, {brightness: 200});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'ON'}), {qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({brightness: 200}), {qos: 0, retain: true});
});
it('Should start when state is corrupted', async () => {
@@ -945,44 +952,44 @@ describe('Controller', () => {
controller.mqtt.retainedMessages,
).length;
mockMQTT.publishAsync.mockClear();
await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers
mockMQTTPublishAsync.mockClear();
await vi.advanceTimersByTimeAsync(2500); // before any startup configure triggers
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(retainedMessages);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(retainedMessages);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), {retain: true, qos: 0});
});
it('Should not republish retained messages on MQTT initial connect when retained message are sent', async () => {
await controller.start();
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bridge/info', 'dummy');
await jest.advanceTimersByTimeAsync(2500); // before any startup configure triggers
await vi.advanceTimersByTimeAsync(2500); // before any startup configure triggers
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => {
settings.set(['mqtt', 'force_disable_retain'], true);
// @ts-expect-error private
await controller.mqtt.connect();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
await controller.mqtt.publish('fo', 'bar', {retain: true});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/fo', 'bar', {retain: false, qos: 0});
});
it('Should publish last seen changes', async () => {
settings.set(['advanced', 'last_seen'], 'epoch');
await controller.start();
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const device = devices.remote;
await mockZHEvents.lastSeenChanged({device, reason: 'deviceAnnounce'});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255, last_seen: 1000}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote', stringify({brightness: 255, last_seen: 1000}), {
qos: 0,
retain: true,
});
@@ -992,10 +999,10 @@ describe('Controller', () => {
settings.set(['advanced', 'last_seen'], 'epoch');
await controller.start();
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const device = devices.remote;
await mockZHEvents.lastSeenChanged({device, reason: 'messageEmitted'});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Ignore messages from coordinator', async () => {
@@ -1034,7 +1041,7 @@ describe('Controller', () => {
it('EventBus should handle errors', async () => {
// @ts-expect-error private
const eventbus = controller.eventBus;
const callback = jest.fn().mockImplementation(async () => {
const callback = vi.fn().mockImplementation(async () => {
throw new Error('Whoops!');
});
eventbus.onStateChange({constructor: {name: 'Test'}}, callback);
+37 -37
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman';
@@ -13,7 +13,7 @@ import Availability from '../../lib/extension/availability';
import * as settings from '../../lib/util/settings';
import utils from '../../lib/util/utils';
const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.info];
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.info];
returnDevices.push(
devices.bulb_color.ieeeAddr,
@@ -37,22 +37,22 @@ describe('Extension: Availability', () => {
};
const setTimeAndAdvanceTimers = async (value: number): Promise<void> => {
jest.setSystemTime(Date.now() + value);
await jest.advanceTimersByTimeAsync(value);
vi.setSystemTime(Date.now() + value);
await vi.advanceTimersByTimeAsync(value);
};
beforeAll(async () => {
jest.spyOn(utils, 'sleep').mockImplementation();
jest.useFakeTimers();
vi.spyOn(utils, 'sleep').mockImplementation(vi.fn());
vi.useFakeTimers();
settings.reRead();
settings.set(['availability'], {enabled: true});
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
beforeEach(async () => {
jest.setSystemTime(utils.minutes(1));
vi.setSystemTime(utils.minutes(1));
data.writeDefaultConfiguration();
settings.reRead();
settings.set(['availability'], {enabled: true});
@@ -67,16 +67,16 @@ describe('Extension: Availability', () => {
afterAll(async () => {
await controller.stop();
jest.useRealTimers();
vi.useRealTimers();
});
it('Should publish availability on startup for device where it is enabled for', async () => {
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {
retain: true,
qos: 1,
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'online'}), {retain: true, qos: 1});
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2/availability', stringify({state: 'online'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'online'}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2/availability', stringify({state: 'online'}), {
retain: true,
qos: 1,
});
@@ -106,14 +106,14 @@ describe('Extension: Availability', () => {
});
it('Should publish offline for active device when not seen for 10 minutes', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(5));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
await setTimeAndAdvanceTimers(utils.minutes(7));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {
retain: true,
qos: 1,
});
@@ -127,20 +127,20 @@ describe('Extension: Availability', () => {
});
it('Should publish offline for passive device when not seen for 25 hours', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.hours(26));
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1});
});
it('Should reset ping timer when device last seen changes for active device', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(5));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {
retain: true,
qos: 1,
});
@@ -154,7 +154,7 @@ describe('Extension: Availability', () => {
});
it('Should ping again when first ping fails', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
@@ -169,13 +169,13 @@ describe('Extension: Availability', () => {
});
it('Should reset ping timer when device last seen changes for passive device', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.hours(24));
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
await mockZHEvents.lastSeenChanged({device: devices.remote});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1});
await setTimeAndAdvanceTimers(utils.hours(25));
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
@@ -185,10 +185,10 @@ describe('Extension: Availability', () => {
});
it('Should immediately mark device as online when it lastSeen changes', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(15));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'offline'}), {
retain: true,
qos: 1,
});
@@ -196,7 +196,7 @@ describe('Extension: Availability', () => {
devices.bulb_color.lastSeen = Date.now();
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', stringify({state: 'online'}), {
retain: true,
qos: 1,
});
@@ -310,18 +310,18 @@ describe('Extension: Availability', () => {
});
it('Should republish availability when device is renamed', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/rename', stringify({from: 'bulb_color', to: 'bulb_new_name'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', stringify({state: 'online'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability', '', {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', stringify({state: 'online'}), {
retain: true,
qos: 1,
});
await setTimeAndAdvanceTimers(utils.hours(12));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', stringify({state: 'offline'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_new_name/availability', stringify({state: 'offline'}), {
retain: true,
qos: 1,
});
@@ -330,10 +330,10 @@ describe('Extension: Availability', () => {
it('Should publish availability payload in JSON format', async () => {
await resetExtension();
devices.remote.ping.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.hours(26));
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', stringify({state: 'offline'}), {retain: true, qos: 1});
});
it('Should publish availability for groups', async () => {
@@ -341,21 +341,21 @@ describe('Extension: Availability', () => {
await resetExtension();
devices.bulb_color_2.ping.mockClear();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {
retain: true,
qos: 1,
});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await setTimeAndAdvanceTimers(utils.minutes(12));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'offline'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'offline'}), {
retain: true,
qos: 1,
});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
devices.bulb_color_2.lastSeen = Date.now();
await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote/availability', stringify({state: 'online'}), {
retain: true,
qos: 1,
});
@@ -365,7 +365,7 @@ describe('Extension: Availability', () => {
// @ts-expect-error private
const availability = controller.extensions.find((extension) => extension instanceof Availability)!;
// @ts-expect-error private
const publishAvailabilitySpy = jest.spyOn(availability, 'publishAvailability');
const publishAvailabilitySpy = vi.spyOn(availability, 'publishAvailability');
devices.bulb_color.ping.mockImplementationOnce(() => new Promise((resolve) => setTimeout(resolve, 1000)));
// @ts-expect-error private
@@ -380,7 +380,7 @@ describe('Extension: Availability', () => {
expect(availability.pingQueue).toEqual([]);
// Validate the stop-interrupt implicitly by checking that it prevents further function invocations
expect(publishAvailabilitySpy).not.toHaveBeenCalled();
devices.bulb_color.ping = jest.fn(); // ensure reset
devices.bulb_color.ping = vi.fn(); // ensure reset
});
it('Should prevent instance restart', async () => {
+31 -31
View File
@@ -1,7 +1,7 @@
import * as data from '../mocks/data';
import {mockDebounce} from '../mocks/debounce';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {Device, devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
@@ -12,7 +12,7 @@ import * as settings from '../../lib/util/settings';
const mocksClear = [
mockDebounce,
mockMQTT.publishAsync,
mockMQTTPublishAsync,
devices.bulb_color.getEndpoint(1)!.configureReporting,
devices.bulb_color.getEndpoint(1)!.bind,
devices.bulb_color_2.getEndpoint(1)!.read,
@@ -31,15 +31,15 @@ describe('Extension: Bind', () => {
endpoint.read.mockClear();
endpoint.write.mockClear();
endpoint.configureReporting.mockClear();
endpoint.bind = jest.fn();
endpoint.bind = vi.fn();
endpoint.bind.mockClear();
endpoint.unbind.mockClear();
}
};
beforeAll(async () => {
jest.useFakeTimers();
controller = new Controller(jest.fn(), jest.fn());
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
@@ -53,7 +53,7 @@ describe('Extension: Bind', () => {
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
it('Should bind to device and configure reporting', async () => {
@@ -92,7 +92,7 @@ describe('Extension: Bind', () => {
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
transaction: '1234',
@@ -108,7 +108,7 @@ describe('Extension: Bind', () => {
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
// Teardown
target.binds = originalTargetBinds;
@@ -116,10 +116,10 @@ describe('Extension: Bind', () => {
});
it('Should throw error on invalid payload', async () => {
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({fromz: 'remote', to: 'bulb_color'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: 'Invalid payload'}),
{retain: false, qos: 0},
@@ -168,7 +168,7 @@ describe('Extension: Bind', () => {
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
transaction: '1234',
@@ -184,7 +184,7 @@ describe('Extension: Bind', () => {
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
// Teardown
target.binds = originalTargetBinds;
@@ -238,7 +238,7 @@ describe('Extension: Bind', () => {
{attribute: 'currentX', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 3600, reportableChange: 1},
]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
transaction: '1234',
@@ -254,7 +254,7 @@ describe('Extension: Bind', () => {
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
// Teardown
target.configuredReportings = originalTargetCR;
@@ -271,7 +271,7 @@ describe('Extension: Bind', () => {
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {from: 'remote', from_endpoint: 'default', to: 'bulb_color', clusters: ['genOnOff'], failed: []}, status: 'ok'}),
{retain: false, qos: 0},
@@ -286,7 +286,7 @@ describe('Extension: Bind', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'button'}));
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: 'Nothing to bind'}),
{retain: false, qos: 0},
@@ -335,7 +335,7 @@ describe('Extension: Bind', () => {
{attribute: 'currentY', minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1},
]);
expect(devices.bulb_color.meta.configured).toBe(332242049);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/unbind',
stringify({
data: {from: 'remote', from_endpoint: 'default', to: 'bulb_color', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []},
@@ -361,7 +361,7 @@ describe('Extension: Bind', () => {
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/unbind',
stringify({
data: {from: 'remote', from_endpoint: 'default', to: 'Coordinator', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []},
@@ -392,7 +392,7 @@ describe('Extension: Bind', () => {
expect(target1Member.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [
{attribute: 'currentLevel', maximumReportInterval: 3600, minimumReportInterval: 5, reportableChange: 1},
]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
data: {from: 'remote', from_endpoint: 'default', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []},
@@ -428,7 +428,7 @@ describe('Extension: Bind', () => {
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/unbind',
stringify({
data: {from: 'remote', from_endpoint: 'default', to: 'group_1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []},
@@ -505,7 +505,7 @@ describe('Extension: Bind', () => {
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', target);
expect(endpoint.bind).toHaveBeenCalledWith('genScenes', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
data: {from: 'remote', from_endpoint: 'default', to: '1', clusters: ['genScenes', 'genOnOff', 'genLevelCtrl'], failed: []},
@@ -526,7 +526,7 @@ describe('Extension: Bind', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color'}));
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: 'Failed to bind'}),
{retain: false, qos: 0},
@@ -545,7 +545,7 @@ describe('Extension: Bind', () => {
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
data: {from: 'remote', from_endpoint: 'ep2', to: 'wall_switch_double', to_endpoint: 'right', clusters: ['genOnOff'], failed: []},
@@ -567,7 +567,7 @@ describe('Extension: Bind', () => {
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
data: {from: 'remote', from_endpoint: 2, to: 'wall_switch_double', to_endpoint: 3, clusters: ['genOnOff'], failed: []},
@@ -586,7 +586,7 @@ describe('Extension: Bind', () => {
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('msTemperatureMeasurement', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({
data: {
@@ -611,7 +611,7 @@ describe('Extension: Bind', () => {
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', target);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {from: 'remote', from_endpoint: 'ep2', to: 'wall_switch', clusters: ['genOnOff'], failed: []}, status: 'ok'}),
{retain: false, qos: 0},
@@ -629,7 +629,7 @@ describe('Extension: Bind', () => {
expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', 901);
expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', 901);
expect(endpoint.unbind).toHaveBeenCalledWith('genScenes', 901);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/unbind',
stringify({
data: {
@@ -650,7 +650,7 @@ describe('Extension: Bind', () => {
mockClear(device);
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote_not_existing', to: 'bulb_color'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: "Source device 'remote_not_existing' does not exist"}),
{retain: false, qos: 0},
@@ -665,7 +665,7 @@ describe('Extension: Bind', () => {
stringify({from: 'remote', from_endpoint: 'not_existing_endpoint', to: 'bulb_color'}),
);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'"}),
{retain: false, qos: 0},
@@ -677,7 +677,7 @@ describe('Extension: Bind', () => {
mockClear(device);
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/bind', stringify({from: 'remote', to: 'bulb_color_not_existing'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: "Target device or group 'bulb_color_not_existing' does not exist"}),
{retain: false, qos: 0},
@@ -692,7 +692,7 @@ describe('Extension: Bind', () => {
stringify({from: 'remote', to: 'bulb_color', to_endpoint: 'not_existing_endpoint'}),
);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/bind',
stringify({data: {}, status: 'error', error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
{retain: false, qos: 0},
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {Device, devices, Endpoint, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
@@ -9,7 +9,7 @@ import stringify from 'json-stable-stringify-without-jsonify';
import {Controller} from '../../lib/controller';
import * as settings from '../../lib/util/settings';
const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.debug];
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
describe('Extension: Configure', () => {
let controller: Controller;
@@ -65,10 +65,10 @@ describe('Extension: Configure', () => {
const wait = async (ms: number): Promise<void> => await new Promise((resolve) => setTimeout(resolve, ms));
beforeAll(async () => {
jest.useFakeTimers();
controller = new Controller(jest.fn(), jest.fn());
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
});
beforeEach(async () => {
@@ -77,11 +77,11 @@ describe('Extension: Configure', () => {
mocksClear.forEach((m) => m.mockClear());
coordinatorEndpoint = devices.coordinator.getEndpoint(1)!;
await resetExtension();
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
it('Should configure Router on startup', async () => {
@@ -150,7 +150,7 @@ describe('Extension: Configure', () => {
await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', 'remote');
await flushPromises();
expectRemoteConfigured();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/configure',
stringify({data: {id: 'remote'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -160,7 +160,7 @@ describe('Extension: Configure', () => {
it('Fail to configure via MQTT when device does not exist', async () => {
await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'not_existing_device'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/configure',
stringify({data: {}, status: 'error', error: "Device 'not_existing_device' does not exist"}),
{retain: false, qos: 0},
@@ -173,7 +173,7 @@ describe('Extension: Configure', () => {
});
await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: 'remote'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/configure',
stringify({data: {}, status: 'error', error: 'Failed to configure (Bind timeout after 10s)'}),
{retain: false, qos: 0},
@@ -183,7 +183,7 @@ describe('Extension: Configure', () => {
it('Fail to configure via MQTT when device has no configure', async () => {
await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({id: '0x0017882104a44559', transaction: 20}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/configure',
stringify({data: {}, status: 'error', error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}),
{retain: false, qos: 0},
@@ -193,7 +193,7 @@ describe('Extension: Configure', () => {
it('Handles invalid payload for configure via MQTT', async () => {
await mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/configure', stringify({idx: '0x0017882104a44559'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/configure',
stringify({data: {}, status: 'error', error: 'Invalid payload'}),
{retain: false, qos: 0},
+166 -154
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {mockMQTTEndAsync, events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {devices, mockController as mockZHController, returnDevices} from '../mocks/zigbeeHerdsman';
@@ -22,18 +22,18 @@ describe('Extension: ExternalConverters', () => {
const mockBasePath = path.join(data.mockDir, BASE_DIR);
let controller: Controller;
const existsSyncSpy = jest.spyOn(fs, 'existsSync');
const readdirSyncSpy = jest.spyOn(fs, 'readdirSync');
const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync');
const rmSyncSpy = jest.spyOn(fs, 'rmSync');
const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync');
const existsSyncSpy = vi.spyOn(fs, 'existsSync');
const readdirSyncSpy = vi.spyOn(fs, 'readdirSync');
const mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync');
const rmSyncSpy = vi.spyOn(fs, 'rmSync');
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync');
const zhcAddDefinitionSpy = jest.spyOn(zhc, 'addDefinition');
const zhcRemoveExternalDefinitionsSpy = jest.spyOn(zhc, 'removeExternalDefinitions');
const zhcAddDefinitionSpy = vi.spyOn(zhc, 'addDefinition');
const zhcRemoveExternalDefinitionsSpy = vi.spyOn(zhc, 'removeExternalDefinitions');
const mocksClear = [
mockMQTT.endAsync,
mockMQTT.publishAsync,
mockMQTTEndAsync,
mockMQTTPublishAsync,
mockLogger.debug,
mockLogger.error,
mockZHController.stop,
@@ -60,12 +60,21 @@ describe('Extension: ExternalConverters', () => {
return controller.zigbee.resolveEntity(zhDevice)! as Device;
};
const resetExtension = async (): Promise<void> => {
await controller.enableDisableExtension(false, 'ExternalConverters');
await controller.enableDisableExtension(true, 'ExternalConverters');
};
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
beforeEach(async () => {
@@ -75,8 +84,6 @@ describe('Extension: ExternalConverters', () => {
data.writeDefaultState();
settings.reRead();
returnDevices.push(devices.external_converter_device.ieeeAddr, devices.coordinator.ieeeAddr);
controller = new Controller(jest.fn(), jest.fn());
});
afterEach(async () => {
@@ -85,143 +92,153 @@ describe('Extension: ExternalConverters', () => {
await controller?.stop();
});
it('loads nothing from folder', async () => {
await controller.start();
await flushPromises();
expect(existsSyncSpy).toHaveBeenCalledWith(mockBasePath);
expect(readdirSyncSpy).not.toHaveBeenCalledWith(mockBasePath);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([]), {retain: true, qos: 0});
});
it('loads from folder', async () => {
useAssets();
await controller.start();
await flushPromises();
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'external',
model: 'external_converter_device',
vendor: 'external',
zigbeeModel: ['external_converter_device'],
describe('from folder', () => {
beforeEach(async () => {
controller = new Controller(vi.fn(), vi.fn());
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/converters',
stringify([
{name: 'mock-external-converter-multiple.js', code: getFileCode('mock-external-converter-multiple.js')},
{name: 'mock-external-converter.js', code: getFileCode('mock-external-converter.js')},
]),
{retain: true, qos: 0},
);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, 'mock-external-converter-multiple.js');
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, 'mock-external-converter.js');
expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
mock: 1,
model: 'external_converters_device_1',
zigbeeModel: ['external_converter_device_1'],
vendor: 'external_1',
description: 'external_1',
}),
);
expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
mock: 2,
model: 'external_converters_device_2',
zigbeeModel: ['external_converter_device_2'],
vendor: 'external_2',
description: 'external_2',
}),
);
expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
mock: true,
zigbeeModel: ['external_converter_device'],
vendor: 'external',
model: 'external_converter_device',
description: 'external',
}),
);
const bridgeDevices = mockMQTT.publishAsync.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/devices');
expect(bridgeDevices.length).toBe(1);
expect(JSON.parse(bridgeDevices[0][1])).toEqual(
expect.arrayContaining([
it('loads nothing', async () => {
await controller.start();
await flushPromises();
expect(existsSyncSpy).toHaveBeenCalledWith(mockBasePath);
expect(readdirSyncSpy).not.toHaveBeenCalledWith(mockBasePath);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([]), {retain: true, qos: 0});
});
it('loads converters', async () => {
useAssets();
await controller.start();
await flushPromises();
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'external',
model: 'external_converter_device',
vendor: 'external',
zigbeeModel: ['external_converter_device'],
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/converters',
stringify([
{name: 'mock-external-converter-multiple.js', code: getFileCode('mock-external-converter-multiple.js')},
{name: 'mock-external-converter.js', code: getFileCode('mock-external-converter.js')},
]),
{retain: true, qos: 0},
);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, 'mock-external-converter-multiple.js');
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, 'mock-external-converter.js');
expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
model_id: 'external_converter_device',
supported: true,
definition: expect.objectContaining({
description: 'external',
model: 'external_converter_device',
}),
mock: 1,
model: 'external_converters_device_1',
zigbeeModel: ['external_converter_device_1'],
vendor: 'external_1',
description: 'external_1',
}),
]),
);
});
);
expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
mock: 2,
model: 'external_converters_device_2',
zigbeeModel: ['external_converter_device_2'],
vendor: 'external_2',
description: 'external_2',
}),
);
expect(zhcAddDefinitionSpy).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
mock: true,
zigbeeModel: ['external_converter_device'],
vendor: 'external',
model: 'external_converter_device',
description: 'external',
}),
);
it('saves and removes from MQTT', async () => {
const converterName = 'foo.js';
const converterCode = getFileCode('mock-external-converter.js');
const converterFilePath = path.join(mockBasePath, converterName);
await controller.start();
await flushPromises();
mocksClear.forEach((m) => m.mockClear());
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'Automatically generated definition',
model: 'external_converter_device',
vendor: '',
zigbeeModel: ['external_converter_device'],
const bridgeDevices = mockMQTTPublishAsync.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/devices');
expect(bridgeDevices.length).toBe(1);
expect(JSON.parse(bridgeDevices[0][1])).toEqual(
expect.arrayContaining([
expect.objectContaining({
model_id: 'external_converter_device',
supported: true,
definition: expect.objectContaining({
description: 'external',
model: 'external_converter_device',
}),
}),
]),
);
});
//-- SAVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode}));
await flushPromises();
it('saves and removes from MQTT', async () => {
const converterName = 'foo.js';
const converterCode = getFileCode('mock-external-converter.js');
const converterFilePath = path.join(mockBasePath, converterName);
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'external',
model: 'external_converter_device',
vendor: 'external',
zigbeeModel: ['external_converter_device'],
});
expect(mkdirSyncSpy).toHaveBeenCalledWith(mockBasePath, {recursive: true});
expect(writeFileSyncSpy).toHaveBeenCalledWith(converterFilePath, converterCode, 'utf8');
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(1);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, converterName);
expect(zhcAddDefinitionSpy).toHaveBeenCalledWith(
expect.objectContaining({
mock: true,
zigbeeModel: ['external_converter_device'],
vendor: 'external',
await controller.start();
await flushPromises();
mocksClear.forEach((m) => m.mockClear());
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'Automatically generated definition',
model: 'external_converter_device',
vendor: '',
zigbeeModel: ['external_converter_device'],
});
//-- SAVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode}));
await flushPromises();
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'external',
}),
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([{name: converterName, code: converterCode}]), {
retain: true,
qos: 0,
});
model: 'external_converter_device',
vendor: 'external',
zigbeeModel: ['external_converter_device'],
});
expect(mkdirSyncSpy).toHaveBeenCalledWith(mockBasePath, {recursive: true});
expect(writeFileSyncSpy).toHaveBeenCalledWith(converterFilePath, converterCode, 'utf8');
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(1);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, converterName);
expect(zhcAddDefinitionSpy).toHaveBeenCalledWith(
expect.objectContaining({
mock: true,
zigbeeModel: ['external_converter_device'],
vendor: 'external',
model: 'external_converter_device',
description: 'external',
}),
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/converters',
stringify([{name: converterName, code: converterCode}]),
{
retain: true,
qos: 0,
},
);
//-- REMOVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName}));
await flushPromises();
//-- REMOVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName}));
await flushPromises();
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'Automatically generated definition',
model: 'external_converter_device',
vendor: '',
zigbeeModel: ['external_converter_device'],
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
description: 'Automatically generated definition',
model: 'external_converter_device',
vendor: '',
zigbeeModel: ['external_converter_device'],
});
expect(rmSyncSpy).toHaveBeenCalledWith(converterFilePath, {force: true});
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, converterName);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([]), {retain: true, qos: 0});
});
expect(rmSyncSpy).toHaveBeenCalledWith(converterFilePath, {force: true});
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, converterName);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/converters', stringify([]), {retain: true, qos: 0});
});
it('returns error on invalid code', async () => {
@@ -229,14 +246,13 @@ describe('Extension: ExternalConverters', () => {
const converterCode = 'definetly not a correct javascript code';
const converterFilePath = path.join(mockBasePath, converterName);
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/converter/save',
expect.stringContaining(`"error":"foo.js contains invalid code`),
{retain: false, qos: 0},
@@ -248,14 +264,13 @@ describe('Extension: ExternalConverters', () => {
const converterName = 'invalid.js';
const converterFilePath = path.join(mockBasePath, converterName);
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/converter/remove',
stringify({data: {}, status: 'error', error: `${converterName} (${converterFilePath}) doesn't exists`}),
{retain: false, qos: 0},
@@ -268,8 +283,7 @@ describe('Extension: ExternalConverters', () => {
const converterCode = getFileCode('mock-external-converter.js');
const converterFilePath = path.join(mockBasePath, converterName);
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
const errorMsg = `Invalid definition`;
@@ -281,7 +295,7 @@ describe('Extension: ExternalConverters', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: converterName, code: converterCode}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/converter/save', expect.stringContaining(errorMsg), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/response/converter/save', expect.stringContaining(errorMsg), {
retain: false,
qos: 0,
});
@@ -293,8 +307,7 @@ describe('Extension: ExternalConverters', () => {
const converterCode = getFileCode('mock-external-converter.js');
const converterFilePath = path.join(mockBasePath, converterName);
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
//-- SAVE
@@ -311,7 +324,7 @@ describe('Extension: ExternalConverters', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({name: converterName}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/converter/remove',
stringify({data: {}, status: 'error', error: errorMsg}),
{retain: false, qos: 0},
@@ -320,14 +333,13 @@ describe('Extension: ExternalConverters', () => {
});
it('handles invalid payloads', async () => {
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/save', stringify({name: 'test.js', transaction: 1 /* code */}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/converter/save',
stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 1}),
{retain: false, qos: 0},
@@ -336,7 +348,7 @@ describe('Extension: ExternalConverters', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/converter/remove', stringify({namex: 'test.js', transaction: 2}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/converter/remove',
stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 2}),
{retain: false, qos: 0},
+91 -77
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {mockMQTTEndAsync, events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {devices, mockController as mockZHController, returnDevices} from '../mocks/zigbeeHerdsman';
@@ -18,15 +18,15 @@ describe('Extension: ExternalExtensions', () => {
let controller: Controller;
const mockBasePath = path.join(data.mockDir, BASE_DIR);
const existsSyncSpy = jest.spyOn(fs, 'existsSync');
const readdirSyncSpy = jest.spyOn(fs, 'readdirSync');
const mkdirSyncSpy = jest.spyOn(fs, 'mkdirSync');
const rmSyncSpy = jest.spyOn(fs, 'rmSync');
const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync');
const existsSyncSpy = vi.spyOn(fs, 'existsSync');
const readdirSyncSpy = vi.spyOn(fs, 'readdirSync');
const mkdirSyncSpy = vi.spyOn(fs, 'mkdirSync');
const rmSyncSpy = vi.spyOn(fs, 'rmSync');
const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync');
const mocksClear = [
mockMQTT.endAsync,
mockMQTT.publishAsync,
mockMQTTEndAsync,
mockMQTTPublishAsync,
mockLogger.debug,
mockLogger.error,
mockZHController.stop,
@@ -46,12 +46,21 @@ describe('Extension: ExternalExtensions', () => {
return fs.readFileSync(path.join(__dirname, '..', 'assets', BASE_DIR, fileName), 'utf8');
};
const resetExtension = async (): Promise<void> => {
await controller.enableDisableExtension(false, 'ExternalExtensions');
await controller.enableDisableExtension(true, 'ExternalExtensions');
};
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
beforeEach(async () => {
@@ -60,72 +69,80 @@ describe('Extension: ExternalExtensions', () => {
data.writeDefaultState();
settings.reRead();
returnDevices.splice(0);
controller = new Controller(jest.fn(), jest.fn());
});
afterEach(() => {
fs.rmSync(mockBasePath, {recursive: true, force: true});
});
it('loads nothing from folder', async () => {
await controller.start();
await flushPromises();
expect(existsSyncSpy).toHaveBeenCalledWith(mockBasePath);
expect(readdirSyncSpy).not.toHaveBeenCalledWith(mockBasePath);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([]), {retain: true, qos: 0});
});
it('loads from folder', async () => {
useAssets();
await controller.start();
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example2/extension', 'call2 from constructor', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example2/extension', 'call2 from start', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/extensions',
stringify([
{name: 'example2Extension.js', code: getFileCode('example2Extension.js')},
{name: 'exampleExtension.js', code: getFileCode('exampleExtension.js')},
]),
{retain: true, qos: 0},
);
});
it('saves and removes from MQTT', async () => {
const extensionName = 'foo.js';
const extensionCode = getFileCode('exampleExtension.js');
const extensionFilePath = path.join(mockBasePath, extensionName);
await controller.start();
await flushPromises();
mocksClear.forEach((m) => m.mockClear());
//-- SAVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: extensionName, code: extensionCode}));
await flushPromises();
expect(mkdirSyncSpy).toHaveBeenCalledWith(mockBasePath, {recursive: true});
expect(writeFileSyncSpy).toHaveBeenCalledWith(extensionFilePath, extensionCode, 'utf8');
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([{name: extensionName, code: extensionCode}]), {
retain: true,
qos: 0,
describe('from folder', () => {
beforeEach(async () => {
controller = new Controller(vi.fn(), vi.fn());
});
//-- REMOVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: extensionName}));
await flushPromises();
it('loads nothing', async () => {
await controller.start();
await flushPromises();
expect(rmSyncSpy).toHaveBeenCalledWith(extensionFilePath, {force: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from stop', {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([]), {retain: true, qos: 0});
expect(existsSyncSpy).toHaveBeenCalledWith(mockBasePath);
expect(readdirSyncSpy).not.toHaveBeenCalledWith(mockBasePath);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([]), {retain: true, qos: 0});
});
it('loads extensions', async () => {
useAssets();
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example2/extension', 'call2 from constructor', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example2/extension', 'call2 from start', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/extensions',
stringify([
{name: 'example2Extension.js', code: getFileCode('example2Extension.js')},
{name: 'exampleExtension.js', code: getFileCode('exampleExtension.js')},
]),
{retain: true, qos: 0},
);
});
it('saves and removes from MQTT', async () => {
const extensionName = 'foo.js';
const extensionCode = getFileCode('exampleExtension.js');
const extensionFilePath = path.join(mockBasePath, extensionName);
await controller.start();
await flushPromises();
mocksClear.forEach((m) => m.mockClear());
//-- SAVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: extensionName, code: extensionCode}));
await flushPromises();
expect(mkdirSyncSpy).toHaveBeenCalledWith(mockBasePath, {recursive: true});
expect(writeFileSyncSpy).toHaveBeenCalledWith(extensionFilePath, extensionCode, 'utf8');
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from constructor', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from start', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/extensions',
stringify([{name: extensionName, code: extensionCode}]),
{
retain: true,
qos: 0,
},
);
//-- REMOVE
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: extensionName}));
await flushPromises();
expect(rmSyncSpy).toHaveBeenCalledWith(extensionFilePath, {force: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/example/extension', 'call from stop', {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/extensions', stringify([]), {retain: true, qos: 0});
});
});
it('returns error on invalid code', async () => {
@@ -133,14 +150,13 @@ describe('Extension: ExternalExtensions', () => {
const extensionCode = 'definetly not a correct javascript code';
const extensionFilePath = path.join(mockBasePath, extensionName);
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: extensionName, code: extensionCode}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/extension/save',
expect.stringContaining(`"error":"${extensionName} contains invalid code`),
{retain: false, qos: 0},
@@ -152,14 +168,13 @@ describe('Extension: ExternalExtensions', () => {
const converterName = 'invalid.js';
const converterFilePath = path.join(mockBasePath, converterName);
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({name: converterName}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/extension/remove',
stringify({data: {}, status: 'error', error: `${converterName} (${converterFilePath}) doesn't exists`}),
{retain: false, qos: 0},
@@ -168,14 +183,13 @@ describe('Extension: ExternalExtensions', () => {
});
it('handles invalid payloads', async () => {
await controller.start();
await flushPromises();
await resetExtension();
mocksClear.forEach((m) => m.mockClear());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/save', stringify({name: 'test.js', transaction: 1 /* code */}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/extension/save',
stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 1}),
{retain: false, qos: 0},
@@ -184,7 +198,7 @@ describe('Extension: ExternalExtensions', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/extension/remove', stringify({namex: 'test.js', transaction: 2}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/extension/remove',
stringify({data: {}, status: 'error', error: `Invalid payload`, transaction: 2}),
{retain: false, qos: 0},
+59 -54
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT} from '../mocks/mqtt';
import {mockMQTTPublishAsync} from '../mocks/mqtt';
import {EventHandler, flushPromises} from '../mocks/utils';
import {devices} from '../mocks/zigbeeHerdsman';
@@ -15,26 +15,26 @@ import * as settings from '../../lib/util/settings';
let mockHTTPOnRequest: (request: {url: string}, response: number) => void;
const mockHTTPEvents: Record<string, EventHandler> = {};
const mockHTTP = {
listen: jest.fn(),
listen: vi.fn(),
on: (event: string, handler: EventHandler): void => {
mockHTTPEvents[event] = handler;
},
close: jest.fn().mockImplementation((cb) => cb()),
close: vi.fn<(cb: (err?: Error) => void) => void>((cb) => cb()),
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let mockHTTPSOnRequest: (request: {url: string}, response: number) => void;
const mockHTTPSEvents: Record<string, EventHandler> = {};
const mockHTTPS = {
listen: jest.fn(),
listen: vi.fn(),
on: (event: string, handler: EventHandler): void => {
mockHTTPSEvents[event] = handler;
},
close: jest.fn().mockImplementation((cb) => cb()),
close: vi.fn<(cb: (err?: Error) => void) => void>((cb) => cb()),
};
const mockWSocket = {
close: jest.fn(),
close: vi.fn<(cb: (err?: Error) => void) => void>(),
};
const mockWSClientEvents: Record<string, EventHandler> = {};
@@ -42,8 +42,8 @@ const mockWSClient = {
on: (event: string, handler: EventHandler): void => {
mockWSClientEvents[event] = handler;
},
send: jest.fn(),
terminate: jest.fn(),
send: vi.fn<(data: string) => void>(),
terminate: vi.fn<() => void>(),
readyState: 'close',
};
const mockWSEvents: Record<string, EventHandler> = {};
@@ -53,57 +53,62 @@ const mockWS = {
on: (event: string, handler: EventHandler): void => {
mockWSEvents[event] = handler;
},
handleUpgrade: jest.fn().mockImplementation((request, socket, head, cb) => {
handleUpgrade: vi.fn().mockImplementation((request, socket, head, cb) => {
cb(mockWSocket);
}),
emit: jest.fn(),
close: jest.fn(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emit: vi.fn<(eventName: string, ...args: any[]) => void>(),
close: vi.fn<(code?: number, data?: string | Buffer) => void>(),
};
let mockNodeStaticPath: string = '';
const mockNodeStatic = jest.fn();
const mockNodeStatic = vi.fn();
const mockFinalHandler = jest.fn();
const mockFinalHandler = vi.fn();
jest.mock('node:http', () => ({
createServer: jest.fn().mockImplementation((onRequest) => {
vi.mock('node:http', () => ({
createServer: vi.fn().mockImplementation((onRequest) => {
mockHTTPOnRequest = onRequest;
return mockHTTP;
}),
Agent: jest.fn(),
Agent: vi.fn(),
}));
jest.mock('node:https', () => ({
createServer: jest.fn().mockImplementation((onRequest) => {
vi.mock('node:https', () => ({
createServer: vi.fn().mockImplementation((onRequest) => {
mockHTTPSOnRequest = onRequest;
return mockHTTPS;
}),
Agent: jest.fn(),
Agent: vi.fn(),
}));
jest.mock('express-static-gzip', () =>
jest.fn().mockImplementation((path) => {
vi.mock('express-static-gzip', () => ({
default: vi.fn().mockImplementation((path) => {
mockNodeStaticPath = path;
return mockNodeStatic;
}),
);
jest.mock('zigbee2mqtt-frontend', () => ({
getPath: (): string => 'my/dummy/path',
}));
jest.mock('ws', () => ({
OPEN: 'open',
Server: jest.fn().mockImplementation(() => {
return mockWS;
}),
vi.mock('zigbee2mqtt-frontend', () => ({
default: {
getPath: (): string => 'my/dummy/path',
},
}));
jest.mock('finalhandler', () =>
jest.fn().mockImplementation(() => {
vi.mock('ws', () => ({
default: {
OPEN: 'open',
Server: vi.fn().mockImplementation(() => {
return mockWS;
}),
},
}));
vi.mock('finalhandler', () => ({
default: vi.fn().mockImplementation(() => {
return mockFinalHandler;
}),
);
}));
const mocksClear = [
mockHTTP.close,
@@ -118,7 +123,7 @@ const mocksClear = [
mockWSClient.terminate,
mockNodeStatic,
mockFinalHandler,
mockMQTT.publishAsync,
mockMQTTPublishAsync,
mockLogger.error,
];
@@ -126,7 +131,7 @@ describe('Extension: Frontend', () => {
let controller: Controller;
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
});
beforeEach(async () => {
@@ -142,7 +147,7 @@ describe('Extension: Frontend', () => {
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
afterEach(async () => {
@@ -150,7 +155,7 @@ describe('Extension: Frontend', () => {
});
it('Start/stop with defaults', async () => {
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockNodeStaticPath).toBe('my/dummy/path');
expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
@@ -163,7 +168,7 @@ describe('Extension: Frontend', () => {
it('Start/stop without host', async () => {
settings.set(['frontend'], {enabled: true, port: 8081});
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockNodeStaticPath).toBe('my/dummy/path');
expect(mockHTTP.listen).toHaveBeenCalledWith(8081);
@@ -176,7 +181,7 @@ describe('Extension: Frontend', () => {
it('Start/stop unix socket', async () => {
settings.set(['frontend', 'host'], '/tmp/zigbee2mqtt.sock');
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockNodeStaticPath).toBe('my/dummy/path');
expect(mockHTTP.listen).toHaveBeenCalledWith('/tmp/zigbee2mqtt.sock');
@@ -190,7 +195,7 @@ describe('Extension: Frontend', () => {
it('Start/stop HTTPS valid', async () => {
settings.set(['frontend', 'ssl_cert'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.crt'));
settings.set(['frontend', 'ssl_key'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.key'));
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockHTTP.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1');
expect(mockHTTPS.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
@@ -199,7 +204,7 @@ describe('Extension: Frontend', () => {
it('Start/stop HTTPS invalid : missing config', async () => {
settings.set(['frontend', 'ssl_cert'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.crt'));
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
expect(mockHTTPS.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1');
@@ -209,7 +214,7 @@ describe('Extension: Frontend', () => {
it('Start/stop HTTPS invalid : missing file', async () => {
settings.set(['frontend', 'ssl_cert'], 'filesNotExists.crt');
settings.set(['frontend', 'ssl_key'], path.join(__dirname, '..', 'assets', 'certs', 'dummy.key'));
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
expect(mockHTTPS.listen).not.toHaveBeenCalledWith(8081, '127.0.0.1');
@@ -217,25 +222,25 @@ describe('Extension: Frontend', () => {
});
it('Websocket interaction', async () => {
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
mockWSClient.readyState = 'open';
mockWS.clients.push(mockWSClient);
await mockWSEvents.connection(mockWSClient);
const allTopics = mockWSClient.send.mock.calls.map((m) => JSON.parse(m).topic);
const allTopics = mockWSClient.send.mock.calls.map(([m]) => JSON.parse(m).topic);
expect(allTopics).toContain('bridge/devices');
expect(allTopics).toContain('bridge/info');
expect(mockWSClient.send).toHaveBeenCalledWith(stringify({topic: 'bridge/state', payload: {state: 'online'}}));
expect(mockWSClient.send).toHaveBeenCalledWith(stringify({topic: 'remote', payload: {brightness: 255}}));
// Message
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockWSClient.send.mockClear();
mockWSClientEvents.message(stringify({topic: 'bulb_color/set', payload: {state: 'ON'}}), false);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({
state: 'ON',
@@ -288,10 +293,10 @@ describe('Extension: Frontend', () => {
});
it('onRequest/onUpgrade', async () => {
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
const mockSocket = {destroy: jest.fn()};
const mockSocket = {destroy: vi.fn()};
mockHTTPEvents.upgrade({url: 'http://localhost:8080/api'}, mockSocket, 3);
expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1);
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
@@ -305,7 +310,7 @@ describe('Extension: Frontend', () => {
});
it('Static server', async () => {
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(mockHTTP.listen).toHaveBeenCalledWith(8081, '127.0.0.1');
@@ -314,10 +319,10 @@ describe('Extension: Frontend', () => {
it('Authentification', async () => {
const authToken = 'sample-secure-token';
settings.set(['frontend', 'auth_token'], authToken);
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
const mockSocket = {destroy: jest.fn()};
const mockSocket = {destroy: vi.fn()};
mockHTTPEvents.upgrade({url: '/api'}, mockSocket, mockWSocket);
expect(mockWS.handleUpgrade).toHaveBeenCalledTimes(1);
expect(mockSocket.destroy).toHaveBeenCalledTimes(0);
@@ -340,7 +345,7 @@ describe('Extension: Frontend', () => {
it.each(['/z2m/', '/z2m'])('Works with non-default base url %s', async (baseUrl) => {
settings.set(['frontend', 'base_url'], baseUrl);
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m/api'});
@@ -366,7 +371,7 @@ describe('Extension: Frontend', () => {
it('Works with non-default complex base url', async () => {
const baseUrl = '/z2m-more++/c0mplex.url/';
settings.set(['frontend', 'base_url'], baseUrl);
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
expect(ws.Server).toHaveBeenCalledWith({noServer: true, path: '/z2m-more++/c0mplex.url/api'});
+151 -151
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from '../mocks/zigbeeHerdsman';
@@ -25,21 +25,21 @@ describe('Extension: Groups', () => {
let controller: Controller;
beforeAll(async () => {
jest.useFakeTimers();
controller = new Controller(jest.fn(), jest.fn());
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
beforeEach(() => {
resetGroupMembers();
data.writeDefaultConfiguration();
settings.reRead();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
groups.gledopto_group.command.mockClear();
zhcToZigbee.__clearStore__();
// @ts-expect-error private
@@ -52,21 +52,21 @@ describe('Extension: Groups', () => {
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should not republish identical optimistic group states', async () => {
const device1 = devices.bulb_2;
const device2 = devices.bulb_color_2;
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockZHEvents.message({
data: {onOff: 1},
cluster: 'genOnOff',
@@ -84,13 +84,13 @@ describe('Extension: Groups', () => {
linkquality: 10,
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(6);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_with_tradfri', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/ha_discovery_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(6);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_tradfri_remote', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_2', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_with_tradfri', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/ha_discovery_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should publish state change of all members when a group changes its state', async () => {
@@ -99,12 +99,12 @@ describe('Extension: Groups', () => {
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should not publish state change when group changes state and device is disabled', async () => {
@@ -114,11 +114,11 @@ describe('Extension: Groups', () => {
group.members.push(endpoint);
settings.set(['devices', device.ieeeAddr, 'disabled'], true);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should publish state change for group when members state change', async () => {
@@ -128,37 +128,37 @@ describe('Extension: Groups', () => {
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should publish state of device with endpoint name', async () => {
const group = groups.gledopto_group;
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/gledopto_group/set', stringify({state: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
});
@@ -166,12 +166,12 @@ describe('Extension: Groups', () => {
it('Should publish state of group when specific state of specific endpoint is changed', async () => {
const group = groups.gledopto_group;
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/GLEDOPTO_2ID/set', stringify({state_cct: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/GLEDOPTO_2ID', stringify({state_cct: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/gledopto_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(group.command).toHaveBeenCalledTimes(0);
});
@@ -182,15 +182,15 @@ describe('Extension: Groups', () => {
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', retain: false, filtered_attributes: ['brightness']}});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON', brightness: 100}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 100}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 100}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Shouldnt publish group state change when a group is not optimistic', async () => {
@@ -200,13 +200,13 @@ describe('Extension: Groups', () => {
group.members.push(endpoint);
settings.set(['groups'], {1: {friendly_name: 'group_1', optimistic: false, retain: false}});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint, type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should publish state change of another group with shared device when a group changes its state', async () => {
@@ -215,13 +215,13 @@ describe('Extension: Groups', () => {
groups.group_1.members.push(endpoint);
groups.group_2.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should not publish state change off if any lights within are still on when changed via device', async () => {
@@ -235,12 +235,12 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used', async () => {
@@ -257,13 +257,13 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Should not publish state change off if any lights within with non default-ep are still on when changed via device', async () => {
@@ -277,12 +277,12 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Should not publish state change off if any lights within are still on when changed via device with non default-ep', async () => {
@@ -299,12 +299,12 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/set', stringify({state_left: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF', state_right: 'ON'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF', state_right: 'ON'}), {
retain: false,
qos: 0,
});
@@ -324,15 +324,15 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await mockMQTTEvents.message('zigbee2mqtt/wall_switch_double/set', stringify({state_left: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_left: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Should not publish state change off if any lights within are still on when changed via shared group', async () => {
@@ -346,13 +346,13 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_2/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_2', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Should publish state change off if all lights within turn off', async () => {
@@ -369,15 +369,15 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({state: 'OFF'}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Should only update group state with changed properties', async () => {
@@ -391,26 +391,26 @@ describe('Extension: Groups', () => {
settings.set(['groups'], {
1: {friendly_name: 'group_1', retain: false},
});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', color_temp: 200}));
await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'ON', color_temp: 250}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 300}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({color_mode: 'color_temp', color_temp: 300, state: 'OFF'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}), {
retain: true,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/group_1',
stringify({color_mode: 'color_temp', color_temp: 300, state: 'ON'}),
{retain: false, qos: 0},
@@ -431,16 +431,16 @@ describe('Extension: Groups', () => {
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({state: 'ON'}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// @ts-expect-error private
controller.state.state = {};
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({state: 'OFF'}), {retain: false, qos: 0});
});
it('Add to group via MQTT', async () => {
@@ -448,15 +448,15 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(1);
const group = groups.group_1;
expect(group.members.length).toBe(0);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/add',
stringify({transaction: '123', group: 'group_1', device: 'bulb_color'}),
);
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, transaction: '123', status: 'ok'}),
{retain: false, qos: 0},
@@ -472,12 +472,12 @@ describe('Extension: Groups', () => {
throw new Error('timeout');
});
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color'}));
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {}, status: 'error', error: 'Failed to add from group (timeout)'}),
{retain: false, qos: 0},
@@ -490,12 +490,12 @@ describe('Extension: Groups', () => {
const group = groups['group/with/slashes'];
settings.set(['groups'], {99: {friendly_name: 'group/with/slashes', retain: false}});
expect(group.members.length).toBe(0);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group/with/slashes', device: 'bulb_color'}));
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group/with/slashes'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -507,15 +507,15 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
expect(group.members.length).toBe(0);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/add',
stringify({group: 'group_1', device: 'wall_switch_double', endpoint: 'right'}),
);
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {device: 'wall_switch_double', endpoint: 'right', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -527,7 +527,7 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
expect(group.members.length).toBe(0);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/add',
stringify({group: 'group_1', device: 'wall_switch_double', endpoint: 'right'}),
@@ -539,8 +539,8 @@ describe('Extension: Groups', () => {
);
await flushPromises();
expect(group.members).toStrictEqual([endpoint]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {device: 'wall_switch_double', endpoint: 'right', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -552,12 +552,12 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1', device: 'bulb_color'}));
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove',
stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -569,15 +569,15 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(1)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/remove',
stringify({group: 'group_1', device: 'bulb_color', skip_disable_reporting: true}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove',
stringify({data: {device: 'bulb_color', endpoint: 'default', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -589,15 +589,15 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/remove',
stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: '3'}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove',
stringify({data: {device: '0x0017880104e45542', endpoint: '3', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -609,15 +609,15 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/remove',
stringify({group: 'group_1', device: 'wall_switch_double', endpoint: '3'}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove',
stringify({data: {device: 'wall_switch_double', endpoint: '3', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -629,15 +629,15 @@ describe('Extension: Groups', () => {
const endpoint = device.getEndpoint(3)!;
const group = groups.group_1;
group.members.push(endpoint);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/remove',
stringify({group: 'group_1', device: '0x0017880104e45542', endpoint: 'right'}),
);
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove',
stringify({data: {device: '0x0017880104e45542', endpoint: 'right', group: 'group_1'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -647,12 +647,12 @@ describe('Extension: Groups', () => {
it('Remove from group all', async () => {
const group = groups.group_1;
groups.group_1.members.push(devices.QBKG03LM.endpoints[2]);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove_all', stringify({device: '0x0017880104e45542', endpoint: 'right'}));
await flushPromises();
expect(group.members).toStrictEqual([]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove_all',
stringify({data: {device: '0x0017880104e45542', endpoint: 'right'}, status: 'ok'}),
{retain: false, qos: 0},
@@ -661,11 +661,11 @@ describe('Extension: Groups', () => {
it('Error when adding to non-existing group', async () => {
mockLogger.error.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/remove', stringify({group: 'group_1_not_existing', device: 'bulb_color'}));
await flushPromises();
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/remove',
stringify({data: {}, status: 'error', error: "Group 'group_1_not_existing' does not exist"}),
{retain: false, qos: 0},
@@ -674,11 +674,11 @@ describe('Extension: Groups', () => {
it('Error when adding a non-existing device', async () => {
mockLogger.error.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', device: 'bulb_color_not_existing'}));
await flushPromises();
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {}, status: 'error', error: "Device 'bulb_color_not_existing' does not exist"}),
{retain: false, qos: 0},
@@ -687,14 +687,14 @@ describe('Extension: Groups', () => {
it('Error when adding a non-existing endpoint', async () => {
mockLogger.error.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message(
'zigbee2mqtt/bridge/request/group/members/add',
stringify({group: 'group_1', device: 'bulb_color', endpoint: 'not_existing_endpoint'}),
);
await flushPromises();
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {}, status: 'error', error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
{retain: false, qos: 0},
@@ -703,11 +703,11 @@ describe('Extension: Groups', () => {
it('Error when invalid payload', async () => {
mockLogger.error.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({group: 'group_1', devicez: 'bulb_color'}));
await flushPromises();
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {}, status: 'error', error: 'Invalid payload'}),
{retain: false, qos: 0},
@@ -716,11 +716,11 @@ describe('Extension: Groups', () => {
it('Error when add/remove with invalid payload', async () => {
mockLogger.error.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/group/members/add', stringify({groupz: 'group_1', device: 'bulb_color'}));
await flushPromises();
expect(mockMQTT.publishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/group/members/add',
stringify({data: {}, status: 'error', error: 'Invalid payload'}),
{retain: false, qos: 0},
@@ -734,38 +734,38 @@ describe('Extension: Groups', () => {
group.members.push(bulbColor.getEndpoint(1)!);
group.members.push(bulbColorTemp.getEndpoint(1)!);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color_temp: 50}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 50}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 50}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 50}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/group_1', stringify({color_mode: 'color_temp', color_temp: 50}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 50}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 50}), {
retain: true,
qos: 0,
});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_1/set', stringify({color: {x: 0.5, y: 0.3}}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/group_1',
stringify({color: {x: 0.5, y: 0.3}, color_mode: 'xy', color_temp: 548}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 548}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({color_mode: 'color_temp', color_temp: 548}), {
retain: true,
qos: 0,
});
File diff suppressed because it is too large Load Diff
+23 -26
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import * as mockSleep from '../mocks/sleep';
import {flushPromises} from '../mocks/utils';
import {devices, events as mockZHEvents, returnDevices} from '../mocks/zigbeeHerdsman';
@@ -23,7 +23,7 @@ returnDevices.push(
devices.external_converter_device.ieeeAddr,
);
const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.debug];
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
describe('Extension: NetworkMap', () => {
let controller: Controller;
@@ -96,9 +96,9 @@ describe('Extension: NetworkMap', () => {
};
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
mockSleep.mock();
jest.spyOn(Date, 'now').mockReturnValue(10000);
vi.spyOn(Date, 'now').mockReturnValue(10000);
data.writeDefaultConfiguration();
settings.reRead();
data.writeEmptyState();
@@ -107,7 +107,7 @@ describe('Extension: NetworkMap', () => {
path.join(__dirname, '..', 'assets', 'external_converters', 'mock-external-converter.js'),
path.join(data.mockDir, 'external_converters', 'mock-external-converter.js'),
);
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
});
@@ -124,15 +124,15 @@ describe('Extension: NetworkMap', () => {
afterAll(async () => {
mockSleep.restore();
fs.rmSync(path.join(data.mockDir, 'external_converters'), {recursive: true});
jest.useRealTimers();
vi.useRealTimers();
});
it('Output raw networkmap', async () => {
mock();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = {
data: {
@@ -315,7 +315,7 @@ describe('Extension: NetworkMap', () => {
},
status: 'ok',
};
const actual = JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]);
const actual = JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]);
expect(actual).toStrictEqual(expected);
});
@@ -329,8 +329,8 @@ describe('Extension: NetworkMap', () => {
await mockZHEvents.message(payload);
mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'graphviz', routes: true}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = `digraph G {
node[shape=record];
@@ -350,7 +350,7 @@ describe('Extension: NetworkMap', () => {
}`;
const expectedLines = expected.split('\n');
const actualLines = JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]).data.value.split('\n');
const actualLines = JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]).data.value.split('\n');
for (let i = 0; i < expectedLines.length; i++) {
expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim());
@@ -367,8 +367,8 @@ describe('Extension: NetworkMap', () => {
await mockZHEvents.message(payload);
mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'plantuml', routes: true}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = `' paste into: https://www.planttext.com/
@@ -451,7 +451,7 @@ describe('Extension: NetworkMap', () => {
@enduml`;
const expectedLines = expected.split('\n');
const actualLines = JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]).data.value.split('\n');
const actualLines = JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]).data.value.split('\n');
for (let i = 0; i < expectedLines.length; i++) {
expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim());
@@ -460,11 +460,10 @@ describe('Extension: NetworkMap', () => {
it('Should throw error when requesting invalid type', async () => {
mock();
mockMQTT.publishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', 'not_existing');
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/networkmap',
stringify({data: {}, status: 'error', error: "Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}),
{retain: false, qos: 0},
@@ -474,11 +473,10 @@ describe('Extension: NetworkMap', () => {
it('Should exclude disabled devices from networkmap', async () => {
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
mock();
mockMQTT.publishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = {
data: {
@@ -619,7 +617,7 @@ describe('Extension: NetworkMap', () => {
},
status: 'ok',
};
const actual = JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]);
const actual = JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]);
expect(actual).toStrictEqual(expected);
});
@@ -627,11 +625,10 @@ describe('Extension: NetworkMap', () => {
settings.set(['devices', '0x000b57fffec6a5b2', 'disabled'], true);
mock();
devices.bulb.lqi.mockRejectedValueOnce('failed');
mockMQTT.publishAsync.mockClear();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/networkmap', stringify({type: 'raw', routes: true}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/response/networkmap');
const expected = {
data: {
@@ -772,7 +769,7 @@ describe('Extension: NetworkMap', () => {
},
status: 'ok',
};
const actual = JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]);
const actual = JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]);
expect(actual).toStrictEqual(expected);
});
});
+9 -9
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT} from '../mocks/mqtt';
import {mockMQTTPublishAsync} from '../mocks/mqtt';
import {flushPromises} from '../mocks/utils';
import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
@@ -9,23 +9,23 @@ import * as zhc from 'zigbee-herdsman-converters';
import {Controller} from '../../lib/controller';
import * as settings from '../../lib/util/settings';
const mockOnEvent = jest.fn();
const mockLivoloOnEvent = jest.fn();
const mockOnEvent = vi.fn();
const mockLivoloOnEvent = vi.fn();
const mappedLivolo = zhc.findByModel('TI0001')!;
mappedLivolo.onEvent = mockLivoloOnEvent;
// @ts-expect-error mock
zhc.onEvent = mockOnEvent;
const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.debug];
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
describe('Extension: OnEvent', () => {
let controller: Controller;
beforeEach(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
data.writeDefaultConfiguration();
settings.reRead();
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
@@ -40,7 +40,7 @@ describe('Extension: OnEvent', () => {
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
});
it('Should call with start event', async () => {
@@ -81,9 +81,9 @@ describe('Extension: OnEvent', () => {
);
// Test deviceExposesChanged
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
console.log(mockLivoloOnEvent.mock.calls[0][5].deviceExposesChanged());
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
});
it('Should call index onEvent with zigbee event', async () => {
+31 -31
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import * as mockSleep from '../mocks/sleep';
import {flushPromises} from '../mocks/utils';
import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
@@ -15,7 +15,7 @@ import * as zhc from 'zigbee-herdsman-converters';
import {Controller} from '../../lib/controller';
import * as settings from '../../lib/util/settings';
const mocksClear = [mockMQTT.publishAsync, devices.bulb.save, mockLogger.info];
const mocksClear = [mockMQTTPublishAsync, devices.bulb.save, mockLogger.info];
const DEFAULT_CONFIG: zhc.Ota.Settings = {
dataDir: data.mockDir,
@@ -25,8 +25,8 @@ const DEFAULT_CONFIG: zhc.Ota.Settings = {
describe('Extension: OTAUpdate', () => {
let controller: Controller;
const updateSpy = jest.spyOn(zhc.ota, 'update');
const isUpdateAvailableSpy = jest.spyOn(zhc.ota, 'isUpdateAvailable');
const updateSpy = vi.spyOn(zhc.ota, 'update');
const isUpdateAvailableSpy = vi.spyOn(zhc.ota, 'isUpdateAvailable');
const resetExtension = async (): Promise<void> => {
await controller.enableDisableExtension(false, 'OTAUpdate');
@@ -34,19 +34,19 @@ describe('Extension: OTAUpdate', () => {
};
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
mockSleep.mock();
data.writeDefaultConfiguration();
settings.reRead();
settings.reRead();
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
await controller.start();
await flushPromises();
});
afterAll(async () => {
mockSleep.restore();
jest.useRealTimers();
vi.useRealTimers();
});
beforeEach(async () => {
@@ -111,21 +111,21 @@ describe('Extension: OTAUpdate', () => {
expect(devices.bulb.save).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'});
expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({update: {state: 'updating', progress: 0}}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({update: {state: 'updating', progress: 0}}), {
retain: true,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update: {state: 'updating', progress: 10, remaining: 3600}}),
{retain: true, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update: {state: 'idle', installed_version: 90, latest_version: 90}}),
{retain: true, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/update',
stringify({
data: {
@@ -137,7 +137,7 @@ describe('Extension: OTAUpdate', () => {
}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
});
it('Should handle when OTA update fails', async () => {
@@ -149,8 +149,8 @@ describe('Extension: OTAUpdate', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({update: {state: 'available'}}), {retain: true, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({update: {state: 'available'}}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/update',
stringify({data: {}, status: 'error', error: "Update of 'bulb' failed (Update failed)"}),
{retain: false, qos: 0},
@@ -164,20 +164,20 @@ describe('Extension: OTAUpdate', () => {
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(1, devices.bulb, {}, undefined, false);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}),
{retain: false, qos: 0},
);
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(2);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(2, devices.bulb, {}, undefined, false);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}),
{retain: false, qos: 0},
@@ -188,7 +188,7 @@ describe('Extension: OTAUpdate', () => {
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(3);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(3, devices.bulb, {}, undefined, true);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}),
{retain: false, qos: 0},
@@ -199,14 +199,14 @@ describe('Extension: OTAUpdate', () => {
const originalDefinition = device.definition;
device.definition = Object.assign({}, originalDefinition, {ota: {suppressElementImageParseFailure: true}});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check/downgrade', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(4);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(4, devices.bulb, {suppressElementImageParseFailure: true}, undefined, true);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}),
{retain: false, qos: 0},
@@ -222,7 +222,7 @@ describe('Extension: OTAUpdate', () => {
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({
data: {},
@@ -236,7 +236,7 @@ describe('Extension: OTAUpdate', () => {
it('Should fail when device does not exist', async () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'not_existing_deviceooo');
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {}, status: 'error', error: `Device 'not_existing_deviceooo' does not exist`}),
{retain: false, qos: 0},
@@ -246,7 +246,7 @@ describe('Extension: OTAUpdate', () => {
it('Should not check for OTA when device does not support it', async () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'dimmer_wall_switch');
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {}, status: 'error', error: `Device 'dimmer_wall_switch' does not support OTA updates`}),
{retain: false, qos: 0},
@@ -267,9 +267,9 @@ describe('Extension: OTAUpdate', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
jest.runOnlyPendingTimers();
vi.runOnlyPendingTimers();
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {}, status: 'error', error: `Update or check for update already in progress for 'bulb'`}),
{retain: false, qos: 0},
@@ -278,11 +278,11 @@ describe('Extension: OTAUpdate', () => {
it('Shouldnt crash when read modelID before/after OTA update fails', async () => {
devices.bulb.endpoints[0].read.mockRejectedValueOnce('Failed from').mockRejectedValueOnce('Failed to');
updateSpy.mockImplementation();
updateSpy.mockImplementationOnce(vi.fn());
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/update',
stringify({data: {id: 'bulb', from: undefined, to: undefined}, status: 'ok'}),
{retain: false, qos: 0},
@@ -319,7 +319,7 @@ describe('Extension: OTAUpdate', () => {
await mockZHEvents.message(payload);
await flushPromises();
expect(mockLogger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update: {state: 'available', installed_version: 10, latest_version: 12}}),
{retain: true, qos: 0},
@@ -344,7 +344,7 @@ describe('Extension: OTAUpdate', () => {
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {}, {imageType: 12382}, false);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({update: {state: 'idle'}}), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bulb', stringify({update: {state: 'idle'}}), {retain: true, qos: 0});
});
it('Should check for update when device requests it and it is not available', async () => {
@@ -365,7 +365,7 @@ describe('Extension: OTAUpdate', () => {
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {}, {imageType: 12382}, false);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
stringify({update: {state: 'idle', installed_version: 13, latest_version: 13}}),
{retain: true, qos: 0},
@@ -427,7 +427,7 @@ describe('Extension: OTAUpdate', () => {
});
it('Sets given configuration', async () => {
const setConfiguration = jest.spyOn(zhc.ota, 'setConfiguration');
const setConfiguration = vi.spyOn(zhc.ota, 'setConfiguration');
settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json');
settings.set(['ota', 'image_block_response_delay'], 10000);
settings.set(['ota', 'default_maximum_data_size'], 10);
+194 -194
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import * as mockSleep from '../mocks/sleep';
import {flushPromises} from '../mocks/utils';
import {devices, groups, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
@@ -13,7 +13,7 @@ import {Controller} from '../../lib/controller';
import {loadTopicGetSetRegex} from '../../lib/extension/publish';
import * as settings from '../../lib/util/settings';
const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.debug];
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
describe('Extension: Publish', () => {
let controller: Controller;
@@ -32,9 +32,9 @@ describe('Extension: Publish', () => {
};
beforeAll(async () => {
jest.useFakeTimers();
vi.useFakeTimers();
data.writeEmptyState();
controller = new Controller(jest.fn(), jest.fn());
controller = new Controller(vi.fn(), vi.fn());
mockSleep.mock();
await controller.start();
await flushPromises();
@@ -62,8 +62,8 @@ describe('Extension: Publish', () => {
});
afterAll(async () => {
await jest.runOnlyPendingTimersAsync();
jest.useRealTimers();
await vi.runOnlyPendingTimersAsync();
vi.useRealTimers();
mockSleep.restore();
});
@@ -73,10 +73,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should corretly handle mallformed messages', async () => {
@@ -106,10 +106,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices when brightness is in %', async () => {
@@ -119,10 +119,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 235, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices when brightness is in number', async () => {
@@ -132,10 +132,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 230, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices with color_temp', async () => {
@@ -145,10 +145,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 222, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 222});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices with color_temp in %', async () => {
@@ -158,10 +158,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 500, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: 'color_temp', color_temp: 500});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices with non-default ep', async () => {
@@ -171,10 +171,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices with non-default ep and postfix', async () => {
@@ -184,10 +184,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/wall_switch_double');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state_right: 'OFF'});
expect(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/wall_switch_double');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state_right: 'OFF'});
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices with endpoint ID', async () => {
@@ -197,7 +197,7 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_right: 'OFF'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/wall_switch_double', stringify({state_right: 'OFF'}), {
qos: 0,
retain: false,
});
@@ -210,10 +210,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices to non default-ep with brightness_[EP]', async () => {
@@ -223,7 +223,7 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN', stringify({brightness_l2: 50, state_l2: 'ON'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/QS-Zigbee-D02-TRIAC-2C-LN', stringify({brightness_l2: 50, state_l2: 'ON'}), {
retain: false,
qos: 0,
});
@@ -241,7 +241,7 @@ describe('Extension: Publish', () => {
{dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/TS0601_switch', stringify({state_l2: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/TS0601_switch', stringify({state_l2: 'ON'}), {retain: false, qos: 0});
});
it('Should publish messages to Tuya cover switch with dummy endpoints', async () => {
@@ -270,7 +270,7 @@ describe('Extension: Publish', () => {
{dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/TS0601_cover_switch',
stringify({state_l2: 'OFF', state_l1: 'ON', state: 'OPEN'}),
{retain: false, qos: 0},
@@ -284,10 +284,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color_mode: 'xy', color: {x: 100, y: 50}});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices with color xy and state', async () => {
@@ -298,8 +298,8 @@ describe('Extension: Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON'}),
{retain: false, qos: 0},
@@ -314,8 +314,8 @@ describe('Extension: Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'ON', brightness: 20}),
{retain: false, qos: 0},
@@ -330,8 +330,8 @@ describe('Extension: Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({color: {x: 100, y: 50}, state: 'ON', brightness: 20, color_mode: 'xy'}),
{retain: false, qos: 0},
@@ -346,8 +346,8 @@ describe('Extension: Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenNthCalledWith(1, 'lightingColorCtrl', 'moveToColor', {colorx: 6553500, colory: 3276750, transtime: 0}, {});
expect(endpoint.command).toHaveBeenNthCalledWith(2, 'genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({color_mode: 'xy', color: {x: 100, y: 50}, state: 'OFF'}),
{retain: false, qos: 0},
@@ -361,9 +361,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'});
});
it('Should publish messages to zigbee devices with color rgb string', async () => {
@@ -373,9 +373,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'});
});
it('Should publish messages to zigbee devices with brightness', async () => {
@@ -385,9 +385,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50});
});
it('Should publish messages groups', async () => {
@@ -397,9 +397,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON'});
group.members.pop();
});
@@ -410,9 +410,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 128, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 128});
group.members.pop();
});
@@ -437,9 +437,9 @@ describe('Extension: Publish', () => {
console.log(mockLogger.warning.mock.calls);
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/hue_twilight_group');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/hue_twilight_group');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON'});
});
it('Should publish messages to groups with on and brightness', async () => {
@@ -449,9 +449,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 50});
group.members.pop();
});
@@ -462,9 +462,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'OFF'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'OFF'});
group.members.pop();
});
@@ -475,9 +475,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 24248, colory: 18350, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({color: {x: 0.37, y: 0.28}, color_mode: 'xy'});
group.members.pop();
});
@@ -488,9 +488,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // 'zigbee2mqtt/bulb_color' + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({color_temp: 100, color_mode: 'color_temp'});
group.members.pop();
});
@@ -515,7 +515,7 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Shouldnt publish new brightness state when filtered_optimistic is used', async () => {
@@ -526,15 +526,15 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
});
it('Shouldnt publish new state when optimistic = false for group', async () => {
settings.set(['groups', '2', 'optimistic'], false);
await mockMQTTEvents.message('zigbee2mqtt/group_2/set', stringify({brightness: '200'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Should handle non-valid topics', async () => {
@@ -681,9 +681,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('lightingColorCtrl', 'moveToColor', {colorx: 17806, colory: 43155, transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}, color_mode: 'xy'});
});
it('Should parse set with ieeeAddr topic', async () => {
@@ -730,13 +730,13 @@ describe('Extension: Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync.mock.calls[0]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
'zigbee2mqtt/wall_switch_double',
stringify({state_left: 'ON'}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[1]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
'zigbee2mqtt/wall_switch_double',
stringify({state_left: 'OFF'}),
{qos: 0, retain: false},
@@ -803,25 +803,25 @@ describe('Extension: Publish', () => {
it('Should also use on/off cluster when controlling group with switch', async () => {
const group = groups.group_with_switch;
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
group.command.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/switch_group/set', stringify({state: 'ON', brightness: 100}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(2);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 0}, {});
expect(group.command).toHaveBeenCalledWith('genOnOff', 'on', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON', brightness: 100}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON', brightness: 100}), {
retain: false,
qos: 0,
});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
group.command.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/switch_group/set', stringify({state: 'OFF'}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'OFF', brightness: 100}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'OFF', brightness: 100}), {
retain: false,
qos: 0,
});
@@ -835,9 +835,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: 100, transtime: 200}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2); // zigbee2mqtt/bulb_color + below
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/group_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON', brightness: 100});
group.members.pop();
});
@@ -885,24 +885,24 @@ describe('Extension: Publish', () => {
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 200}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(3, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(3, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(4, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(4, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF', brightness: 1}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(5, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(5, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 200}), {
retain: false,
qos: 0,
});
@@ -922,8 +922,8 @@ describe('Extension: Publish', () => {
await flushPromises();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'ON', transition: 0.5}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 254}), {
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON', brightness: 254}), {
retain: false,
qos: 0,
});
@@ -1014,18 +1014,18 @@ describe('Extension: Publish', () => {
expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync.mock.calls[0]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 50}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[1]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'OFF', brightness: 0}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[2]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[2]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 50}),
{qos: 0, retain: false},
@@ -1045,18 +1045,18 @@ describe('Extension: Publish', () => {
expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]);
expect(endpoint.command.mock.calls[2]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 50, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync.mock.calls[0]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 50}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[1]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'OFF', brightness: 0}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[2]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[2]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 50}),
{qos: 0, retain: false},
@@ -1076,9 +1076,9 @@ describe('Extension: Publish', () => {
{direction: 0, enhancehue: 45510, saturation: 127, transtime: 0},
{},
]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color: {hue: 250, saturation: 50}, color_mode: 'hs'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {hue: 250, saturation: 50}, color_mode: 'hs'});
});
it('ZNCLDJ11LM open', async () => {
@@ -1149,9 +1149,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 10}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
});
it('When device is turned off and on with transition with report enabled it should restore correct brightness', async () => {
@@ -1161,15 +1161,15 @@ describe('Extension: Publish', () => {
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200}));
await flushPromises();
endpoint.command.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// Turn off
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'OFF', brightness: 200}),
{qos: 0, retain: false},
@@ -1185,8 +1185,8 @@ describe('Extension: Publish', () => {
linkquality: 10,
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync.mock.calls[1]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'OFF', brightness: 1}),
{qos: 0, retain: false},
@@ -1197,8 +1197,8 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 30}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync.mock.calls[2]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[2]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 200}),
{qos: 0, retain: false},
@@ -1212,15 +1212,15 @@ describe('Extension: Publish', () => {
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: 200}));
await flushPromises();
endpoint.command.mockClear();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
// Turn off
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF', transition: 3}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 0, transtime: 30}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'OFF', brightness: 200}),
{qos: 0, retain: false},
@@ -1231,8 +1231,8 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 200, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync.mock.calls[1]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 200}),
{qos: 0, retain: false},
@@ -1248,9 +1248,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
});
it('Home Assistant: should not set state when color temperature is also set and device is already on', async () => {
@@ -1267,9 +1267,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100, color_mode: 'color_temp'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100, color_mode: 'color_temp'});
});
it('Home Assistant: should set state when color temperature is also set and device is off', async () => {
@@ -1287,8 +1287,8 @@ describe('Extension: Publish', () => {
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(['lightingColorCtrl', 'moveToColorTemp', {colortemp: 100, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', color_temp: 100, color_mode: 'color_temp'}),
{retain: false, qos: 0},
@@ -1309,9 +1309,9 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command.mock.calls[0]).toEqual(['lightingColorCtrl', 'moveToColor', {colorx: 26869, colory: 16384, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON', color_mode: 'xy'});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON', color_mode: 'xy'});
});
it('Should publish correct state on toggle command to zigbee bulb', async () => {
@@ -1322,13 +1322,13 @@ describe('Extension: Publish', () => {
// At this point the bulb has no state yet, so we cannot determine the next state and therefore shouldn't publish it to mockMQTT.
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'toggle', {}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
// Turn bulb off so that the bulb gets a state.
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'OFF'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({state: 'OFF'}), {retain: false, qos: 0});
// Toggle again, now that we have state it should publish state ON
endpoint.read.mockImplementationOnce((cluster, attrs) => {
@@ -1339,8 +1339,8 @@ describe('Extension: Publish', () => {
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({state: 'TOGGLE'}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(2, 'zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should publish messages with options disableDefaultResponse', async () => {
@@ -1350,20 +1350,20 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genOnOff', 'off', {}, {disableDefaultResponse: true});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'OFF'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish messages to zigbee devices', async () => {
settings.set(['advanced', 'last_seen'], 'ISO_8601');
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({brightness: '200'}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(typeof JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]).last_seen).toStrictEqual('string');
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color');
expect(typeof JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]).last_seen).toStrictEqual('string');
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish brightness_move up to zigbee devices', async () => {
@@ -1451,10 +1451,10 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith('genLevelCtrl', 'moveToLevelWithOnOff', {level: '255', transtime: 0}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({position: 100});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/roller_shutter');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({position: 100});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish to MKS-CM-W5', async () => {
@@ -1469,10 +1469,10 @@ describe('Extension: Publish', () => {
{dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/MKS-CM-W5');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state_l3: 'ON'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish separate genOnOff to GL-S-007ZS when setting state and brightness as bulb doesnt turn on with moveToLevelWithOnOff', async () => {
@@ -1481,11 +1481,11 @@ describe('Extension: Publish', () => {
const endpoint = device.getEndpoint(1)!;
await mockMQTTEvents.message('zigbee2mqtt/GL-S-007ZS/set', stringify({state: 'ON', brightness: 20}));
await flushPromises();
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(['genOnOff', 'on', {}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(['genLevelCtrl', 'moveToLevelWithOnOff', {level: 20, transtime: 0}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/GL-S-007ZS', stringify({state: 'ON', brightness: 20}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/GL-S-007ZS', stringify({state: 'ON', brightness: 20}), {
qos: 0,
retain: false,
});
@@ -1568,19 +1568,19 @@ describe('Extension: Publish', () => {
expect(endpoint.command.mock.calls[2]).toEqual(['genOnOff', 'off', {}, {}]);
expect(endpoint.command.mock.calls[3]).toEqual(['genOnOff', 'on', {}, {}]);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(4);
expect(mockMQTT.publishAsync.mock.calls[0]).toEqual(['zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {qos: 0, retain: false}]);
expect(mockMQTT.publishAsync.mock.calls[1]).toEqual([
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(4);
expect(mockMQTTPublishAsync.mock.calls[0]).toEqual(['zigbee2mqtt/bulb_color', stringify({state: 'ON'}), {qos: 0, retain: false}]);
expect(mockMQTTPublishAsync.mock.calls[1]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 150}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[2]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[2]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'OFF', brightness: 150}),
{qos: 0, retain: false},
]);
expect(mockMQTT.publishAsync.mock.calls[3]).toEqual([
expect(mockMQTTPublishAsync.mock.calls[3]).toEqual([
'zigbee2mqtt/bulb_color',
stringify({state: 'ON', brightness: 150}),
{qos: 0, retain: false},
@@ -1597,63 +1597,63 @@ describe('Extension: Publish', () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genScenes', 'store', {groupid: 15071, sceneid: 1}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), {retain: true, qos: 0});
await mockMQTTEvents.message('zigbee2mqtt/bulb_color_2/set', stringify({state: 'ON', brightness: 250, color_temp: 20}));
await mockMQTTEvents.message('zigbee2mqtt/bulb_2/set', stringify({state: 'ON', brightness: 110}));
await flushPromises();
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
group.command.mockClear();
await mockMQTTEvents.message('zigbee2mqtt/group_tradfri_remote/set', stringify({scene_recall: 1}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith('genScenes', 'recall', {groupid: 15071, sceneid: 1}, {});
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(8);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(8);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
1,
'zigbee2mqtt/group_tradfri_remote',
stringify({brightness: 50, color_temp: 290, state: 'ON', color_mode: 'color_temp'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
2,
'zigbee2mqtt/bulb_color_2',
stringify({color_mode: 'color_temp', brightness: 50, color_temp: 290, state: 'ON'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
3,
'zigbee2mqtt/ha_discovery_group',
stringify({brightness: 50, color_mode: 'color_temp', color_temp: 290, state: 'ON'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
4,
'zigbee2mqtt/group_tradfri_remote',
stringify({brightness: 100, color_temp: 290, state: 'ON', color_mode: 'color_temp'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
5,
'zigbee2mqtt/bulb_2',
stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
6,
'zigbee2mqtt/group_with_tradfri',
stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
7,
'zigbee2mqtt/switch_group',
stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
8,
'zigbee2mqtt/ha_discovery_group',
stringify({brightness: 100, color_mode: 'color_temp', color_temp: 290, state: 'ON'}),
@@ -1668,18 +1668,18 @@ describe('Extension: Publish', () => {
await flushPromises();
await mockMQTTEvents.message('zigbee2mqtt/bulb_color/set', stringify({color_temp: 300}));
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 100}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, 'zigbee2mqtt/bulb_color', stringify({color_mode: 'color_temp', color_temp: 100}), {
retain: false,
qos: 0,
});
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
2,
'zigbee2mqtt/bulb_color',
stringify({color: {x: 0.1, y: 0.5}, color_mode: 'xy', color_temp: 79}),
{retain: false, qos: 0},
);
expect(mockMQTT.publishAsync).toHaveBeenNthCalledWith(
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(
3,
'zigbee2mqtt/bulb_color',
stringify({color: {x: 0.4152, y: 0.3954}, color_mode: 'color_temp', color_temp: 300}),
+139 -142
View File
@@ -1,6 +1,6 @@
import * as data from '../mocks/data';
import {mockLogger} from '../mocks/logger';
import {mockMQTT, events as mockMQTTEvents} from '../mocks/mqtt';
import {events as mockMQTTEvents, mockMQTTPublishAsync} from '../mocks/mqtt';
import * as mockSleep from '../mocks/sleep';
import {flushPromises} from '../mocks/utils';
import {devices, events as mockZHEvents} from '../mocks/zigbeeHerdsman';
@@ -10,17 +10,17 @@ import stringify from 'json-stable-stringify-without-jsonify';
import {Controller} from '../../lib/controller';
import * as settings from '../../lib/util/settings';
const mocksClear = [mockMQTT.publishAsync, mockLogger.warning, mockLogger.debug];
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
describe('Extension: Receive', () => {
let controller: Controller;
beforeAll(async () => {
jest.useFakeTimers();
controller = new Controller(jest.fn(), jest.fn());
vi.useFakeTimers();
controller = new Controller(vi.fn(), vi.fn());
mockSleep.mock();
await controller.start();
await jest.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
});
beforeEach(async () => {
@@ -33,7 +33,7 @@ describe('Extension: Receive', () => {
});
afterAll(async () => {
jest.useRealTimers();
vi.useRealTimers();
mockSleep.restore();
});
@@ -44,8 +44,8 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single', linkquality: 10}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single', linkquality: 10}), {
retain: false,
qos: 0,
});
@@ -57,10 +57,10 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({action: 'single_left'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({action: 'single_left'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should handle a zigbee message which uses ep (right)', async () => {
@@ -69,10 +69,10 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(2), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({action: 'single_right'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({action: 'single_right'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should handle a zigbee message with default precision', async () => {
@@ -88,10 +88,10 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: -0.85});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: -0.85});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
});
it('Should allow to invert cover', async () => {
@@ -107,14 +107,14 @@ describe('Extension: Receive', () => {
linkquality: 10,
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {
retain: false,
qos: 0,
});
// Inverted
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
settings.set(['devices', device.ieeeAddr, 'invert_cover'], true);
await mockZHEvents.message({
data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80},
@@ -125,8 +125,8 @@ describe('Extension: Receive', () => {
linkquality: 10,
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {
retain: false,
qos: 0,
});
@@ -139,8 +139,8 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/button', stringify({action: 'single'}), {retain: false, qos: 0});
});
it('Should debounce messages', async () => {
@@ -177,14 +177,14 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload3);
await flushPromises();
jest.advanceTimersByTime(50);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
jest.runOnlyPendingTimers();
vi.advanceTimersByTime(50);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
vi.runOnlyPendingTimers();
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
});
it('Should debounce and retain messages when set via device_options', async () => {
@@ -223,14 +223,14 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload3);
await flushPromises();
jest.advanceTimersByTime(50);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
jest.runOnlyPendingTimers();
vi.advanceTimersByTime(50);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
vi.runOnlyPendingTimers();
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true});
});
it('Should debounce messages only with the same payload values for provided debounce_ignore keys', async () => {
@@ -274,20 +274,19 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(humidityMsg);
await flushPromises();
jest.advanceTimersByTime(50);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2});
jest.runOnlyPendingTimers();
vi.advanceTimersByTime(50);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2});
vi.runOnlyPendingTimers();
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03});
});
it('Should NOT publish old messages from State cache during debouncing', async () => {
// Summary:
// First send multiple measurements to device that is debouncing. Make sure only one message is sent out to mockMQTT. This also ensures first message is cached to "State".
// Then send another measurement to that same device and trigger asynchronous event to push data from Cache. Newest value should be sent out.
const device = devices.WSDCGQ11LM;
settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1);
await mockZHEvents.message({
@@ -315,16 +314,15 @@ describe('Extension: Receive', () => {
linkquality: 10,
});
await flushPromises();
jest.advanceTimersByTime(50);
vi.advanceTimersByTime(50);
// Test that measurements are combined(=debounced)
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
jest.runOnlyPendingTimers();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
vi.runOnlyPendingTimers();
// Test that only one MQTT is sent out and test its values.
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
// Send another Zigbee message...
await mockZHEvents.message({
@@ -340,16 +338,15 @@ describe('Extension: Receive', () => {
// Trigger asynchronous event while device is "debouncing" to trigger Message to be sent out from State cache.
await controller.publishEntityState(realDevice, {});
jest.runOnlyPendingTimers();
await flushPromises();
vi.runOnlyPendingTimers();
// Total of 3 messages should have triggered.
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
// Test that message pushed by asynchronous message contains NEW measurement and not old.
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
// Test that messages after debouncing contains NEW measurement and not old.
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
});
it('Should throttle multiple messages from spamming devices', async () => {
@@ -390,22 +387,22 @@ describe('Extension: Receive', () => {
await mockZHEvents.message(payload3);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/spammer1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.01});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/spammer1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.01});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: true});
// Now we try after elapsed time to see if it publishes next message
const timeshift = throttle_for_testing * 2000;
jest.advanceTimersByTime(timeshift);
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
vi.advanceTimersByTime(timeshift);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
await flushPromises();
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/spammer1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.03});
expect(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true});
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/spammer1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.03});
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: true});
const data4 = {measuredValue: 4};
const payload4 = {
@@ -419,10 +416,10 @@ describe('Extension: Receive', () => {
await mockZHEvents.message(payload4);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTT.publishAsync.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/spammer1');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[2][1])).toStrictEqual({temperature: 0.04});
expect(mockMQTT.publishAsync.mock.calls[2][2]).toStrictEqual({qos: 0, retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
expect(mockMQTTPublishAsync.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/spammer1');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[2][1])).toStrictEqual({temperature: 0.04});
expect(mockMQTTPublishAsync.mock.calls[2][2]).toStrictEqual({qos: 0, retain: true});
});
it('Shouldnt republish old state', async () => {
@@ -439,10 +436,10 @@ describe('Extension: Receive', () => {
});
await mockMQTTEvents.message('zigbee2mqtt/bulb/set', stringify({state: 'ON'}));
await flushPromises();
jest.runOnlyPendingTimers();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON'});
vi.runOnlyPendingTimers();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: 'ON'});
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: 'ON'});
});
it('Should handle a zigbee message with 1 precision', async () => {
@@ -459,10 +456,10 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: -0.8});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: -0.8});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
});
it('Should handle a zigbee message with 0 precision', async () => {
@@ -479,10 +476,10 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: -1});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: -1});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
});
it('Should handle a zigbee message with 1 precision when set via device_options', async () => {
@@ -499,10 +496,10 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: -0.8});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: -0.8});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
});
it('Should handle a zigbee message with 2 precision when overrides device_options', async () => {
@@ -520,10 +517,10 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({temperature: -1});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: -1});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
});
it('Should handle a zigbee message with voltage 2990', async () => {
@@ -532,10 +529,10 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish 1 message when converted twice', async () => {
@@ -544,16 +541,16 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({
battery: 100,
illuminance: 381,
voltage: 3045,
device_temperature: 19,
power_outage_count: 34,
});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish 1 message when converted twice', async () => {
@@ -562,7 +559,7 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Should publish last_seen epoch', async () => {
@@ -572,10 +569,10 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(typeof JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]).last_seen).toBe('number');
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(typeof JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]).last_seen).toBe('number');
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish last_seen ISO_8601', async () => {
@@ -585,10 +582,10 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(typeof JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]).last_seen).toBe('string');
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(typeof JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]).last_seen).toBe('string');
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should publish last_seen ISO_8601_local', async () => {
@@ -598,10 +595,10 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(typeof JSON.parse(mockMQTT.publishAsync.mock.calls[0][1]).last_seen).toBe('string');
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(typeof JSON.parse(mockMQTTPublishAsync.mock.calls[0][1]).last_seen).toBe('string');
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should handle messages from Xiaomi router devices', async () => {
@@ -610,9 +607,9 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/power_plug', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTT.publishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/power_plug', stringify({state: 'ON'}), {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith('zigbee2mqtt/switch_group', stringify({state: 'ON'}), {retain: false, qos: 0});
});
it('Should not handle messages from coordinator', async () => {
@@ -621,7 +618,7 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
});
it('Should not handle messages from still interviewing devices with unknown definition', async () => {
@@ -631,7 +628,7 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
expect(mockLogger.debug).toHaveBeenCalledWith(`Skipping message, still interviewing`);
});
@@ -649,29 +646,29 @@ describe('Extension: Receive', () => {
};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({action: 'brightness_stop'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({action: 'brightness_stop'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should add elapsed', async () => {
settings.set(['advanced', 'elapsed'], true);
const device = devices.E1743;
const payload = {data: {}, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff'};
jest.spyOn(Date, 'now').mockReturnValueOnce(150).mockReturnValueOnce(200);
vi.spyOn(Date, 'now').mockReturnValueOnce(150).mockReturnValueOnce(200);
await mockZHEvents.message({...payload, meta: {zclTransactionSequenceNumber: 2}});
await flushPromises();
await mockZHEvents.message({...payload, meta: {zclTransactionSequenceNumber: 3}});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({action: 'brightness_stop'});
expect(mockMQTT.publishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTT.publishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1])).toMatchObject({action: 'brightness_stop'});
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[1][1]).elapsed).toBe(50);
expect(mockMQTT.publishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({action: 'brightness_stop'});
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toMatchObject({action: 'brightness_stop'});
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1]).elapsed).toBe(50);
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
});
it('Should log when message is from supported device but has no converters', async () => {
@@ -680,7 +677,7 @@ describe('Extension: Receive', () => {
const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20};
await mockZHEvents.message(payload);
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(0);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
expect(mockLogger.debug).toHaveBeenCalledWith(
"No converter available for 'ZNCZ02LM' with cluster 'genBinaryOutput' and type 'attributeReport' and data '{\"inactiveText\":\"hello\"}'",
);
@@ -702,11 +699,11 @@ describe('Extension: Receive', () => {
meta: {zclTransactionSequenceNumber: 1},
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_NEW');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({energy: 0.66, power: 49.6});
mockMQTT.publishAsync.mockClear();
mockMQTTPublishAsync.mockClear();
const SP600_OLD = devices.SP600_OLD;
await mockZHEvents.message({
data,
@@ -718,9 +715,9 @@ describe('Extension: Receive', () => {
meta: {zclTransactionSequenceNumber: 2},
});
await flushPromises();
expect(mockMQTT.publishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
expect(JSON.parse(mockMQTT.publishAsync.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/SP600_OLD');
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({energy: 6.65, power: 496});
});
it('Should emit DevicesChanged event when a converter announces changed exposes', async () => {
@@ -728,6 +725,6 @@ describe('Extension: Receive', () => {
const data = {deviceMode: 0};
const payload = {data, cluster: 'boschSpecific', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await mockZHEvents.message(payload);
expect(mockMQTT.publishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/devices');
});
});
+11 -9
View File
@@ -1,5 +1,7 @@
import * as data from './mocks/data';
import type {MockInstance} from 'vitest';
import fs from 'node:fs';
import {platform} from 'node:os';
import path from 'node:path';
@@ -12,7 +14,7 @@ import logger from '../lib/util/logger';
import * as settings from '../lib/util/settings';
describe('Logger', () => {
let consoleWriteSpy: jest.SpyInstance;
let consoleWriteSpy: MockInstance;
const dir = tmp.dirSync();
const getCachedNamespacedLevels = (): Record<string, string> => {
@@ -22,7 +24,7 @@ describe('Logger', () => {
beforeAll(() => {
// @ts-expect-error private
consoleWriteSpy = jest.spyOn(console._stdout, 'write').mockImplementation(() => {});
consoleWriteSpy = vi.spyOn(console._stdout, 'write').mockImplementation(() => {});
});
afterAll(() => {
@@ -174,9 +176,9 @@ describe('Logger', () => {
['warning', {higher: ['error'], lower: ['debug', 'info']}],
['error', {higher: [], lower: ['debug', 'info', 'warning']}],
])('Logs relevant levels for %s', (level, otherLevels) => {
logger.setLevel(level);
logger.setLevel(level as settings.LogLevel);
const logSpy = jest.spyOn(logger.winston, 'log');
const logSpy = vi.spyOn(logger.winston, 'log');
consoleWriteSpy.mockClear();
let i = 1;
@@ -262,7 +264,7 @@ describe('Logger', () => {
],
])('Sets namespace ignore for debug level %s', (ignore, expected, tests) => {
logger.setLevel('debug');
const logSpy = jest.spyOn(logger.winston, 'log');
const logSpy = vi.spyOn(logger.winston, 'log');
logger.setDebugNamespaceIgnore(ignore);
// @ts-expect-error private
expect(logger.debugNamespaceIgnoreRegex).toStrictEqual(expected);
@@ -290,7 +292,7 @@ describe('Logger', () => {
expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'warning'});
expect(logger.getLevel()).toStrictEqual('debug');
const logSpy = jest.spyOn(logger.winston, 'log');
const logSpy = vi.spyOn(logger.winston, 'log');
consoleWriteSpy.mockClear();
logger.info(`MQTT publish: topic 'abcd/efgh', payload '{"my": {"payload": "injson"}}'`, 'z2m:mqtt');
@@ -311,7 +313,7 @@ describe('Logger', () => {
expect(logger.getNamespacedLevels()).toStrictEqual({'z2m:mqtt': 'info'});
expect(logger.getLevel()).toStrictEqual('warning');
const logSpy = jest.spyOn(logger.winston, 'log');
const logSpy = vi.spyOn(logger.winston, 'log');
consoleWriteSpy.mockClear();
logger.info(`MQTT publish: topic 'abcd/efgh', payload '{"my": {"payload": "injson"}}'`, 'z2m:mqtt');
@@ -329,7 +331,7 @@ describe('Logger', () => {
});
it('Logs with namespaced levels hierarchy', () => {
const nsLevels = {'zh:zstack': 'debug', 'zh:zstack:unpi:writer': 'error'};
const nsLevels = {'zh:zstack': 'debug' as const, 'zh:zstack:unpi:writer': 'error' as const};
let cachedNSLevels;
cachedNSLevels = Object.assign({}, nsLevels);
logger.setNamespacedLevels(nsLevels);
@@ -383,7 +385,7 @@ describe('Logger', () => {
it('Ignores SPLAT chars', () => {
logger.setLevel('debug');
const logSpy = jest.spyOn(logger.winston, 'log');
const logSpy = vi.spyOn(logger.winston, 'log');
consoleWriteSpy.mockClear();
let splatChars = '%d';
+10 -4
View File
@@ -290,10 +290,16 @@ export function read(): ReturnType<typeof yaml.read> {
return yaml.read(path.join(mockDir, 'configuration.yaml'));
}
jest.mock('../../lib/util/data', () => ({
joinPath: (file: string): string => jest.requireActual('node:path').join(mockDir, file),
getPath: (): string => mockDir,
}));
vi.mock('../../lib/util/data', async () => {
const nodePath = await vi.importActual<typeof import('node:path')>('node:path');
return {
default: {
joinPath: (file: string): string => nodePath.join(mockDir, file),
getPath: (): string => mockDir,
},
};
});
writeDefaultConfiguration();
writeDefaultState();
+4 -2
View File
@@ -1,3 +1,5 @@
export const mockDebounce = jest.fn((fn) => fn);
export const mockDebounce = vi.fn((fn) => fn);
jest.mock('debounce', () => mockDebounce);
vi.mock('debounce', () => ({
default: mockDebounce,
}));
+5 -5
View File
@@ -1,11 +1,11 @@
export const mockJSZipFile = jest.fn();
export const mockJSZipGenerateAsync = jest.fn().mockReturnValue('THISISBASE64');
export const mockJSZipFile = vi.fn();
export const mockJSZipGenerateAsync = vi.fn().mockReturnValue('THISISBASE64');
jest.mock('jszip', () =>
jest.fn().mockImplementation(() => {
vi.mock('jszip', () => ({
default: vi.fn().mockImplementation(() => {
return {
file: mockJSZipFile,
generateAsync: mockJSZipGenerateAsync,
};
}),
);
}));
+13 -11
View File
@@ -1,7 +1,7 @@
import type {LogLevel} from 'lib/util/settings';
import type Transport from 'winston-transport';
let level = 'info';
let level: LogLevel = 'info';
let debugNamespaceIgnore: string = '';
let namespacedLevels: Record<string, LogLevel> = {};
let transports: Transport[] = [];
@@ -9,20 +9,20 @@ let transportsEnabled: boolean = false;
const getMessage = (messageOrLambda: string | (() => string)): string => (messageOrLambda instanceof Function ? messageOrLambda() : messageOrLambda);
export const mockLogger = {
log: jest.fn().mockImplementation((level, message, namespace = 'z2m') => {
log: vi.fn().mockImplementation((level, message, namespace = 'z2m') => {
if (transportsEnabled) {
for (const transport of transports) {
transport.log!({level, message, namespace}, () => {});
}
}
}),
init: jest.fn(),
info: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('info', getMessage(messageOrLambda), namespace)),
warning: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('warning', getMessage(messageOrLambda), namespace)),
error: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('error', getMessage(messageOrLambda), namespace)),
debug: jest.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('debug', getMessage(messageOrLambda), namespace)),
cleanup: jest.fn(),
logOutput: jest.fn(),
init: vi.fn(),
info: vi.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('info', getMessage(messageOrLambda), namespace)),
warning: vi.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('warning', getMessage(messageOrLambda), namespace)),
error: vi.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('error', getMessage(messageOrLambda), namespace)),
debug: vi.fn().mockImplementation((messageOrLambda, namespace = 'z2m') => mockLogger.log('debug', getMessage(messageOrLambda), namespace)),
cleanup: vi.fn(),
logOutput: vi.fn(),
add: (transport: Transport): void => {
transports.push(transport);
},
@@ -47,7 +47,9 @@ export const mockLogger = {
setTransportsEnabled: (value: boolean): void => {
transportsEnabled = value;
},
end: jest.fn(),
end: vi.fn(),
};
jest.mock('../../lib/util/logger', () => mockLogger);
vi.mock('../../lib/util/logger', () => ({
default: mockLogger,
}));
+19 -15
View File
@@ -4,28 +4,32 @@ import {EventHandler} from './utils';
export const events: Record<string, EventHandler> = {};
export const mockMQTT = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
publishAsync: jest.fn(async (topic: string, message: string, opts?: IClientPublishOptions): Promise<void> => {}),
endAsync: jest.fn(async (): Promise<void> => {}),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
subscribeAsync: jest.fn(async (topicObject: string): Promise<void> => {}),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
unsubscribeAsync: jest.fn(async (topic: string): Promise<void> => {}),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const mockMQTTPublishAsync = vi.fn(async (topic: string, message: string, opts?: IClientPublishOptions): Promise<void> => {});
export const mockMQTTEndAsync = vi.fn(async (): Promise<void> => {});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const mockMQTTSubscribeAsync = vi.fn(async (topicObject: string): Promise<void> => {});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const mockMQTTUnsubscribeAsync = vi.fn(async (topic: string): Promise<void> => {});
export const mockMQTTConnectAsync = vi.fn(() => ({
reconnecting: false,
disconnecting: false,
disconnected: false,
on: jest.fn((type, handler) => {
publishAsync: mockMQTTPublishAsync,
endAsync: mockMQTTEndAsync,
subscribeAsync: mockMQTTSubscribeAsync,
unsubscribeAsync: mockMQTTUnsubscribeAsync,
on: vi.fn((type, handler) => {
if (type === 'connect') {
handler();
}
events[type] = handler;
}),
stream: {setMaxListeners: jest.fn()},
};
export const mockMQTTConnectAsync = jest.fn(() => mockMQTT);
stream: {setMaxListeners: vi.fn()},
}));
jest.mock('mqtt', () => {
return {connectAsync: mockMQTTConnectAsync};
});
vi.mock('mqtt', () => ({
connectAsync: mockMQTTConnectAsync,
}));
+2 -2
View File
@@ -1,9 +1,9 @@
import utils from '../../lib/util/utils';
const spy = jest.spyOn(utils, 'sleep');
const spy = vi.spyOn(utils, 'sleep');
export function mock(): void {
spy.mockImplementation();
spy.mockImplementation(vi.fn());
}
export function restore(): void {
+3 -4
View File
@@ -1,10 +1,9 @@
export type EventHandler = (...args: unknown[]) => unknown;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type JestMockAny = jest.Mock<any, any, any>;
export async function flushPromises(): Promise<void> {
const nodeTimers = await vi.importActual<typeof import('node:timers')>('node:timers');
export function flushPromises(): Promise<void> {
return new Promise(jest.requireActual('node:timers').setImmediate);
return await new Promise(nodeTimers.setImmediate);
}
// https://github.com/jestjs/jest/issues/6028#issuecomment-567669082
+74 -74
View File
@@ -1,9 +1,11 @@
import type {Mock} from 'vitest';
import assert from 'node:assert';
import {Zcl} from 'zigbee-herdsman';
import {CoordinatorVersion, DeviceType, NetworkParameters, StartResult} from 'zigbee-herdsman/dist/adapter/tstype';
import {EventHandler, JestMockAny} from './utils';
import {EventHandler} from './utils';
type ZHConfiguredReporting = {
cluster: {name: string};
@@ -94,22 +96,22 @@ export class Endpoint {
ID: number;
inputClusters: number[];
outputClusters: number[];
command: JestMockAny;
commandResponse: JestMockAny;
read: JestMockAny;
write: JestMockAny;
bind: JestMockAny;
unbind: JestMockAny;
save: JestMockAny;
configureReporting: JestMockAny;
command: Mock;
commandResponse: Mock;
read: Mock;
write: Mock;
bind: Mock;
unbind: Mock;
save: Mock;
configureReporting: Mock;
meta: Record<string, unknown>;
binds: ZHBind[];
profileID: number | undefined;
deviceID: number | undefined;
configuredReportings: ZHConfiguredReporting[];
addToGroup: JestMockAny;
removeFromGroup: JestMockAny;
getClusterAttributeValue: JestMockAny;
addToGroup: Mock;
removeFromGroup: Mock;
getClusterAttributeValue: Mock;
constructor(
ID: number,
@@ -128,33 +130,33 @@ export class Endpoint {
this.ID = ID;
this.inputClusters = inputClusters;
this.outputClusters = outputClusters;
this.command = jest.fn();
this.commandResponse = jest.fn();
this.read = jest.fn();
this.write = jest.fn();
this.bind = jest.fn();
this.unbind = jest.fn();
this.save = jest.fn();
this.configureReporting = jest.fn();
this.command = vi.fn();
this.commandResponse = vi.fn();
this.read = vi.fn();
this.write = vi.fn();
this.bind = vi.fn();
this.unbind = vi.fn();
this.save = vi.fn();
this.configureReporting = vi.fn();
this.meta = meta;
this.binds = binds;
this.profileID = profileID;
this.deviceID = deviceID;
this.configuredReportings = configuredReportings;
this.addToGroup = jest.fn((group: Group) => {
this.addToGroup = vi.fn((group: Group) => {
if (!group.members.includes(this)) {
group.members.push(this);
}
});
this.removeFromGroup = jest.fn((group: Group) => {
this.removeFromGroup = vi.fn((group: Group) => {
const index = group.members.indexOf(this);
if (index != -1) {
group.members.splice(index, 1);
}
});
this.getClusterAttributeValue = jest.fn((cluster: string, value: string) =>
this.getClusterAttributeValue = vi.fn((cluster: string, value: string) =>
!(cluster in this.clusterValues) ? undefined : this.clusterValues[cluster][value],
);
}
@@ -231,21 +233,21 @@ export class Device {
softwareBuildID: string | undefined;
interviewCompleted: boolean;
modelID: string | undefined;
interview: JestMockAny;
interview: Mock;
interviewing: boolean;
meta: Record<string, unknown>;
ping: JestMockAny;
removeFromNetwork: JestMockAny;
removeFromDatabase: JestMockAny;
ping: Mock;
removeFromNetwork: Mock;
removeFromDatabase: Mock;
customClusters: Record<string, unknown>;
addCustomCluster: JestMockAny;
save: JestMockAny;
addCustomCluster: Mock;
save: Mock;
manufacturerName: string | undefined;
lastSeen: number | undefined;
isDeleted: boolean;
linkquality?: number;
lqi: JestMockAny;
routingTable: JestMockAny;
lqi: Mock;
routingTable: Mock;
constructor(
type: string,
@@ -272,20 +274,20 @@ export class Device {
this.softwareBuildID = softwareBuildID;
this.interviewCompleted = interviewCompleted;
this.modelID = modelID;
this.interview = jest.fn();
this.interview = vi.fn();
this.interviewing = interviewing;
this.meta = {};
this.ping = jest.fn();
this.removeFromNetwork = jest.fn();
this.removeFromDatabase = jest.fn();
this.ping = vi.fn();
this.removeFromNetwork = vi.fn();
this.removeFromDatabase = vi.fn();
this.customClusters = customClusters;
this.addCustomCluster = jest.fn();
this.save = jest.fn();
this.addCustomCluster = vi.fn();
this.save = vi.fn();
this.manufacturerName = manufacturerName;
this.lastSeen = 1000;
this.isDeleted = false;
this.lqi = jest.fn(() => ({neighbors: []}));
this.routingTable = jest.fn(() => ({table: []}));
this.lqi = vi.fn(() => ({neighbors: []}));
this.routingTable = vi.fn(() => ({table: []}));
}
getEndpoint(ID: number): Endpoint | undefined {
@@ -309,19 +311,19 @@ export class Device {
export class Group {
groupID: number;
command: JestMockAny;
command: Mock;
meta: Record<string, unknown>;
members: Endpoint[];
removeFromDatabase: JestMockAny;
removeFromNetwork: JestMockAny;
removeFromDatabase: Mock;
removeFromNetwork: Mock;
constructor(groupID: number, members: Endpoint[]) {
this.groupID = groupID;
this.command = jest.fn();
this.command = vi.fn();
this.meta = {};
this.members = members;
this.removeFromDatabase = jest.fn();
this.removeFromNetwork = jest.fn();
this.removeFromDatabase = vi.fn();
this.removeFromNetwork = vi.fn();
}
hasMember(endpoint: Endpoint): boolean {
@@ -1121,25 +1123,23 @@ export const mockController = {
on: (type: string, handler: EventHandler): void => {
events[type] = handler;
},
start: jest.fn((): Promise<StartResult> => Promise.resolve('reset')),
stop: jest.fn(),
touchlinkIdentify: jest.fn(),
touchlinkScan: jest.fn(),
touchlinkFactoryReset: jest.fn(),
touchlinkFactoryResetFirst: jest.fn(),
addInstallCode: jest.fn(),
permitJoin: jest.fn(),
getPermitJoin: jest.fn((): boolean => false),
getPermitJoinEnd: jest.fn((): number | undefined => undefined),
isStopping: jest.fn((): boolean => false),
backup: jest.fn(),
coordinatorCheck: jest.fn(),
getCoordinatorVersion: jest.fn((): Promise<CoordinatorVersion> => Promise.resolve({type: 'z-Stack', meta: {version: 1, revision: 20190425}})),
getNetworkParameters: jest.fn(
(): Promise<NetworkParameters> => Promise.resolve({panID: 0x162a, extendedPanID: '0x64c5fd698daf0c00', channel: 15}),
),
getDevices: jest.fn((): Device[] => []),
getDevicesIterator: jest.fn(function* (predicate?: (value: Device) => boolean): Generator<Device> {
start: vi.fn((): Promise<StartResult> => Promise.resolve('reset')),
stop: vi.fn(),
touchlinkIdentify: vi.fn(),
touchlinkScan: vi.fn(),
touchlinkFactoryReset: vi.fn(),
touchlinkFactoryResetFirst: vi.fn(),
addInstallCode: vi.fn(),
permitJoin: vi.fn(),
getPermitJoin: vi.fn((): boolean => false),
getPermitJoinEnd: vi.fn((): number | undefined => undefined),
isStopping: vi.fn((): boolean => false),
backup: vi.fn(),
coordinatorCheck: vi.fn(),
getCoordinatorVersion: vi.fn((): Promise<CoordinatorVersion> => Promise.resolve({type: 'z-Stack', meta: {version: 1, revision: 20190425}})),
getNetworkParameters: vi.fn((): Promise<NetworkParameters> => Promise.resolve({panID: 0x162a, extendedPanID: '0x64c5fd698daf0c00', channel: 15})),
getDevices: vi.fn((): Device[] => []),
getDevicesIterator: vi.fn(function* (predicate?: (value: Device) => boolean): Generator<Device> {
for (const key in devices) {
const device = devices[key as keyof typeof devices];
@@ -1148,24 +1148,24 @@ export const mockController = {
}
}
}),
getDevicesByType: jest.fn((type: DeviceType): Device[] =>
getDevicesByType: vi.fn((type: DeviceType): Device[] =>
Object.values(devices)
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.filter((d) => d.type === type),
),
getDeviceByIeeeAddr: jest.fn((ieeeAddr: string): Device | undefined =>
getDeviceByIeeeAddr: vi.fn((ieeeAddr: string): Device | undefined =>
Object.values(devices)
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.find((d) => d.ieeeAddr === ieeeAddr),
),
getDeviceByNetworkAddress: jest.fn((networkAddress: number): Device | undefined =>
getDeviceByNetworkAddress: vi.fn((networkAddress: number): Device | undefined =>
Object.values(devices)
.filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr))
.find((d) => d.networkAddress === networkAddress),
),
getGroupByID: jest.fn((groupID: number): Group | undefined => Object.values(groups).find((g) => g.groupID === groupID)),
getGroups: jest.fn((): Group[] => []),
getGroupsIterator: jest.fn(function* (predicate?: (value: Group) => boolean): Generator<Group> {
getGroupByID: vi.fn((groupID: number): Group | undefined => Object.values(groups).find((g) => g.groupID === groupID)),
getGroups: vi.fn((): Group[] => []),
getGroupsIterator: vi.fn(function* (predicate?: (value: Group) => boolean): Generator<Group> {
for (const key in groups) {
const group = groups[key as keyof typeof groups];
@@ -1174,14 +1174,14 @@ export const mockController = {
}
}
}),
createGroup: jest.fn((groupID: number): Group => {
createGroup: vi.fn((groupID: number): Group => {
const group = new Group(groupID, []);
groups[`group_${groupID}` as keyof typeof groups] = group;
return group;
}),
};
jest.mock('zigbee-herdsman', () => ({
...jest.requireActual('zigbee-herdsman'),
Controller: jest.fn().mockImplementation(() => mockController),
vi.mock('zigbee-herdsman', async (importOriginal) => ({
...(await importOriginal()),
Controller: vi.fn().mockImplementation(() => mockController),
}));
+2 -2
View File
@@ -1,8 +1,8 @@
{
"extends": "../tsconfig",
"include": ["./**/*"],
"include": ["./**/*", "vitest.config.mts"],
"compilerOptions": {
"types": ["jest"],
"types": ["vitest/globals"],
"rootDir": "..",
"noEmit": true
},
+8 -6
View File
@@ -3,6 +3,12 @@ import path from 'node:path';
import utils from '../lib/util/utils';
const mockGetLastCommit = vi.fn<() => [boolean, {shortHash: string} | null]>(() => [false, {shortHash: '123'}]);
vi.mock('git-last-commit', () => ({
getLastCommit: vi.fn((cb) => cb(...mockGetLastCommit())),
}));
describe('Utils', () => {
it('Object is empty', () => {
expect(utils.objectIsEmpty({})).toBeTruthy();
@@ -16,14 +22,10 @@ describe('Utils', () => {
it('git last commit', async () => {
const version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
let mockReturnValue: [identical: boolean, result: {shortHash: string} | null] = [false, {shortHash: '123'}];
jest.mock('git-last-commit', () => ({
getLastCommit: (cb: (identical: boolean, result: {shortHash: string} | null) => void): void => cb(mockReturnValue[0], mockReturnValue[1]),
}));
expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: '123', version: version});
mockReturnValue = [true, null];
mockGetLastCommit.mockReturnValueOnce([true, null]);
expect(await utils.getZigbee2MQTTVersion()).toStrictEqual({commitHash: expect.any(String), version: version});
});
@@ -40,7 +42,7 @@ describe('Utils', () => {
it('To local iso string', async () => {
const date = new Date('August 19, 1975 23:15:30 UTC+00:00').getTime();
const getTzOffsetSpy = jest.spyOn(Date.prototype, 'getTimezoneOffset');
const getTzOffsetSpy = vi.spyOn(Date.prototype, 'getTimezoneOffset');
getTzOffsetSpy.mockReturnValueOnce(60);
expect(utils.formatDate(date, 'ISO_8601_local').toString().endsWith('-01:00')).toBeTruthy();
getTzOffsetSpy.mockReturnValueOnce(-60);
+26
View File
@@ -0,0 +1,26 @@
import {defineConfig} from 'vitest/config';
export default defineConfig({
test: {
globals: true,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
return false;
},
coverage: {
enabled: false,
provider: 'v8',
include: ['lib/**'],
extension: ['.ts'],
// exclude: [],
clean: true,
cleanOnRerun: true,
reportsDirectory: 'coverage',
reporter: ['text', 'html'],
reportOnFailure: false,
thresholds: {
100: true,
},
},
},
});