From f019b15744822fa6ebcfd549bcc863cb8f1ab383 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 9 Aug 2026 18:03:30 +0200 Subject: [PATCH 1/2] Introduce minimal linting for tests --- eslint.config.ts | 27 ++++++++++++++- package-lock.json | 32 ++++++++++++++++++ package.json | 1 + tests/integration/deviceEvents.spec.ts | 4 +++ tests/unit/automation/scriptRuntime.spec.ts | 8 ++--- .../controller/getDevicesController.spec.ts | 2 +- tests/unit/device/deviceManager.spec.ts | 9 +++-- tests/unit/device/knownDeviceRegistry.spec.ts | 3 +- .../protocol/airotic/airoticDevice.spec.ts | 17 ++++------ .../buttplugIo/buttplugIoDevice.spec.ts | 33 +++++++++---------- .../protocol/estim2b/estim2bDevice.spec.ts | 26 +++++++-------- .../genericSlvCtrlPlusDevice.spec.ts | 16 ++++----- .../slvCtrlPlus/slvCtrlProtocolLegacy.spec.ts | 20 +++++------ .../slvCtrlPlus/slvCtrlProtocolV1.spec.ts | 16 ++++----- .../display/displayVirtualDevice.spec.ts | 7 ++-- .../unit/device/transport/bleObserver.spec.ts | 3 +- .../transport/serialPortObserver.spec.ts | 15 ++------- tests/unit/settings/settingsManager.spec.ts | 8 ++--- 18 files changed, 144 insertions(+), 103 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index e89aaea6..e8fb5b14 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -6,6 +6,7 @@ import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions"; import globals from "globals"; import stylistic from "@stylistic/eslint-plugin"; import sortClassMembers from "eslint-plugin-sort-class-members"; +import vitest from '@vitest/eslint-plugin' export default defineConfig([ globalIgnores(["**/*.{js,cjs,mjs}", "!src/**/*.{js,cjs,mjs}"]), @@ -293,7 +294,31 @@ export default defineConfig([ }, }, { - files: ["src/**/*.{js,cjs,mjs}"], + files: ["tests/**/*.ts"], // or any other pattern + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: ["./tests/tsconfig.json", "./tests/type/tsconfig.json"], + sourceType: "module", + }, + globals: { + ...vitest.environments.env.globals, + }, + }, + settings: { + vitest: { + typecheck: true, + }, + }, + plugins: { + vitest, + }, + rules: { + ...vitest.configs.recommended.rules, // you can also use vitest.configs.all.rules to enable all rules + }, + }, + { + files: ["src/**/*.{js,cjs,mjs}", "tests/**/*.{js,cjs,mjs}"], rules: { "no-restricted-syntax": [ "error", diff --git a/package-lock.json b/package-lock.json index 40b97444..d287a6b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "@types/w3c-web-usb": "^1.0.14", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.0.16", + "@vitest/eslint-plugin": "^1.6.26", "@vitest/expect": "^4.1.10", "eslint": "^10.1.0", "eslint-plugin-jsdoc": "^62.8.0", @@ -2769,6 +2770,37 @@ } } }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.6.26", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.26.tgz", + "integrity": "sha512-2eawZk4MZvkEtMZFdScmCE0R1Rti85uRZpmbe9eAv5Q3blDZ5OxlrC9IHlSnxhDCqOO2xKNaD3oufl9BUlI0yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.58.0", + "@typescript-eslint/utils": "^8.58.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "*", + "eslint": ">=8.57.0", + "typescript": ">=5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", diff --git a/package.json b/package.json index 1b77d713..bcb1836a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/w3c-web-usb": "^1.0.14", "@types/ws": "^8.18.1", "@vitest/coverage-v8": "^4.0.16", + "@vitest/eslint-plugin": "^1.6.26", "@vitest/expect": "^4.1.10", "eslint": "^10.1.0", "eslint-plugin-jsdoc": "^62.8.0", diff --git a/tests/integration/deviceEvents.spec.ts b/tests/integration/deviceEvents.spec.ts index 8a46899d..a94d1ce9 100644 --- a/tests/integration/deviceEvents.spec.ts +++ b/tests/integration/deviceEvents.spec.ts @@ -121,6 +121,8 @@ describe('Device events', () => { settingsManager.replace(settingsWithOneDevice); await deviceDisconnected; + + expect(deviceManager.getConnectedDevices()).toHaveLength(1); }, 1000); it('disabling a known device closes it and re-enabling it reconnects it', async () => { @@ -250,5 +252,7 @@ describe('Device events', () => { await device?.close(); await disconnected; + + expect(deviceManager.getConnectedDevices()).toHaveLength(0); }); }); diff --git a/tests/unit/automation/scriptRuntime.spec.ts b/tests/unit/automation/scriptRuntime.spec.ts index 706b49be..4491ce5e 100644 --- a/tests/unit/automation/scriptRuntime.spec.ts +++ b/tests/unit/automation/scriptRuntime.spec.ts @@ -145,8 +145,7 @@ describe('ScriptRuntime (isolated-vm)', () => { }); it('runForEvent does nothing when not loaded', () => { - // Should not throw - runtime.runForEvent({ type: DeviceManagerEvent.deviceConnected, device: deviceA, args: [] }); + expect(() => runtime.runForEvent({ type: DeviceManagerEvent.deviceConnected, device: deviceA, args: [] })).not.toThrow(); }); it('onStart handler runs before scriptStarted', async () => { @@ -435,8 +434,9 @@ describe('ScriptRuntime (isolated-vm)', () => { }); `); - // Should complete without error - await dispatchAndCollect(eventEmitter, runtime, deviceA, TEST_END_MARKER); + const logs = await dispatchAndCollect(eventEmitter, runtime, deviceA, TEST_END_MARKER); + + expect(logs).toContain(TEST_END_MARKER); }); // ----------------------------------------------------------------------- diff --git a/tests/unit/controller/getDevicesController.spec.ts b/tests/unit/controller/getDevicesController.spec.ts index 69e52a27..214c09b2 100644 --- a/tests/unit/controller/getDevicesController.spec.ts +++ b/tests/unit/controller/getDevicesController.spec.ts @@ -8,7 +8,7 @@ import { createTestDevice } from '../device/testDevice.js'; describe('getDevicesController', () => { - it('it returns all connected devices', async () => { + it('returns all connected devices', async () => { const fwVersion = 10000; const deviceUuid = 'foo-bar-baz'; const deviceName = 'Aston Martin'; diff --git a/tests/unit/device/deviceManager.spec.ts b/tests/unit/device/deviceManager.spec.ts index 44cd7cfa..37e5494b 100644 --- a/tests/unit/device/deviceManager.spec.ts +++ b/tests/unit/device/deviceManager.spec.ts @@ -45,7 +45,7 @@ describe('deviceManager', () => { return offerPromise; }; - it('it adds device to managed devices and emits an event', async () => { + it('adds device to managed devices and emits an event', async () => { const mockedDeviceManagerEventEmitter = mock(); mockedDeviceManagerEventEmitter.emit.mockReturnValue(true); @@ -75,7 +75,7 @@ describe('deviceManager', () => { }); - it('it removes device from managed devices and emits event on disconnect', async () => { + it('removes device from managed devices and emits event on disconnect', async () => { const deviceId = DeviceId.create('test-device-id'); const device = new TestDevice(deviceId, 'Foo', new Date(), false, new EventEmitter()); @@ -100,7 +100,7 @@ describe('deviceManager', () => { expect(mockedLogger.child).toBeCalledWith({ name: DeviceManager.name }); }); - it('it emits an event on device update', async () => { + it('emits an event on device update', async () => { const deviceId = DeviceId.create('test-device-id'); const device = new TestDevice(deviceId, 'Foo', new Date(), false, new EventEmitter()); @@ -189,8 +189,7 @@ describe('deviceManager', () => { manager.announceDetectedDevice(deviceInfo); manager.announceDetectedDevice(deviceInfo); - expect(mockedEventEmitter.emit).toHaveBeenCalledOnce(); - expect(mockedEventEmitter.emit).toHaveBeenCalledWith(DeviceManagerEvent.deviceDetected, deviceInfo); + expect(mockedEventEmitter.emit).toHaveBeenCalledExactlyOnceWith(DeviceManagerEvent.deviceDetected, deviceInfo); }); it('allows re-announcing a device after it was revoked (tombstone must not permanently block it)', () => { diff --git a/tests/unit/device/knownDeviceRegistry.spec.ts b/tests/unit/device/knownDeviceRegistry.spec.ts index 6f55e710..8e23e7ce 100644 --- a/tests/unit/device/knownDeviceRegistry.spec.ts +++ b/tests/unit/device/knownDeviceRegistry.spec.ts @@ -80,8 +80,7 @@ describe('KnownDeviceRegistry', () => { registry.persist(knownDevice); - expect(mockSettings.addKnownDevice).toHaveBeenCalledOnce(); - expect(mockSettings.addKnownDevice).toHaveBeenCalledWith(knownDevice); + expect(mockSettings.addKnownDevice).toHaveBeenCalledExactlyOnceWith(knownDevice); }); it('does not touch settings when persisting an already-known, unchanged identity', () => { diff --git a/tests/unit/device/protocol/airotic/airoticDevice.spec.ts b/tests/unit/device/protocol/airotic/airoticDevice.spec.ts index b6db5a0a..cae7293d 100644 --- a/tests/unit/device/protocol/airotic/airoticDevice.spec.ts +++ b/tests/unit/device/protocol/airotic/airoticDevice.spec.ts @@ -138,8 +138,7 @@ describe('AiroticDevice', () => { await device.setAttribute('resetColors', true); - expect(mockHandler.send).toHaveBeenCalledOnce(); - expect(mockHandler.send).toHaveBeenCalledWith(AiroticProtocol.createResetColorsMessage()); + expect(mockHandler.send).toHaveBeenCalledExactlyOnceWith(AiroticProtocol.createResetColorsMessage()); }); it('does not send any message when value is false', async () => { @@ -165,8 +164,7 @@ describe('AiroticDevice', () => { await device.setAttribute('reboot', true); - expect(mockHandler.send).toHaveBeenCalledOnce(); - expect(mockHandler.send).toHaveBeenCalledWith(AiroticProtocol.createRebootMessage()); + expect(mockHandler.send).toHaveBeenCalledExactlyOnceWith(AiroticProtocol.createRebootMessage()); expect(closeSpy).toHaveBeenCalled(); }); @@ -251,8 +249,7 @@ describe('AiroticDevice', () => { it('registers an onReceive callback on the transport', () => { const device = createDevice(); - expect(mockTransport.onReceive).toHaveBeenCalledOnce(); - expect(mockTransport.onReceive).toHaveBeenCalledWith(expect.any(Function)); + expect(mockTransport.onReceive).toHaveBeenCalledExactlyOnceWith(expect.any(Function)); }); }); @@ -272,8 +269,7 @@ describe('AiroticDevice', () => { onReceiveCb!(Buffer.from('*B', 'utf-8')); - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith(device, { + expect(listener).toHaveBeenCalledExactlyOnceWith(device, { type: 'colorChange', data: { colorType: 'breathInColor' }, }); @@ -285,8 +281,7 @@ describe('AiroticDevice', () => { onReceiveCb!(Buffer.from('*R', 'utf-8')); - expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith(device, { + expect(listener).toHaveBeenCalledExactlyOnceWith(device, { type: 'colorChange', data: { colorType: 'restColor' }, }); @@ -500,7 +495,7 @@ describe('AiroticDevice', () => { }); }); - describe('setAttribute reboot', () => { + describe('setAttribute reboot - close timing', () => { it('calls sleep(500) between sending the reboot message and closing', async () => { const { sleep } = await import('../../../../../src/util/async.js'); const device = createDevice(); diff --git a/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts b/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts index abc6fd6e..2ff31172 100644 --- a/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts +++ b/tests/unit/device/protocol/buttplugIo/buttplugIoDevice.spec.ts @@ -34,7 +34,7 @@ describe('ButtplugIoDevice', () => { ); } - it('it throws an error if non-existing attribute is set', async () => { + it('throws an error if non-existing attribute is set', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -42,14 +42,11 @@ describe('ButtplugIoDevice', () => { const attrName: ButtplugIoDeviceAttributeKey = 'Vibrate-1'; - // Act - const result = expect(device.setAttribute(attrName, false)); - - // Assert - await result.rejects.toThrow(`Attribute with name '${attrName}' does not exist for this device`); + // Act & Assert + await expect(device.setAttribute(attrName, false)).rejects.toThrow(`Attribute with name '${attrName}' does not exist for this device`); }); - it('it updates device data and calls buttplugClientDevice on setting boolean attribute', async () => { + it('updates device data and calls buttplugClientDevice on setting boolean attribute', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -72,7 +69,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.scalar).toHaveBeenCalledWith({ActuatorType: 'Rotate', Index: 1, Scalar: 0}); }); - it('it updates device data and calls buttplugClientDevice on setting range attribute', async () => { + it('updates device data and calls buttplugClientDevice on setting range attribute', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -109,7 +106,7 @@ describe('ButtplugIoDevice', () => { }); }); - it('it updates device data and calls buttplugClientDevice on setting boolean attribute to true', async () => { + it('updates device data and calls buttplugClientDevice on setting boolean attribute to true', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -126,7 +123,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.scalar).toHaveBeenCalledWith({ActuatorType: 'Rotate', Index: 1, Scalar: 1}); }); - it('it throws an error if attribute is read-only', async () => { + it('throws an error if attribute is read-only', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -142,7 +139,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.scalar).not.toHaveBeenCalled(); }); - it('it throws an error for attribute with a sensor-type key', async () => { + it('throws an error for attribute with a sensor-type key', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -158,7 +155,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.scalar).not.toHaveBeenCalled(); }); - it('it throws when setting attribute with undefined value', async () => { + it('throws when setting attribute with undefined value', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -174,7 +171,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.scalar).not.toHaveBeenCalled(); }); - it('it updates device data and calls buttplugClientDevice on setting int attribute', async () => { + it('updates device data and calls buttplugClientDevice on setting int attribute', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -193,7 +190,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.scalar).toHaveBeenCalledWith({ ActuatorType: 'Oscillate', Index: 1, Scalar: newValue }); }); - it('it updates sensor attribute values on refresh', async () => { + it('updates sensor attribute values on refresh', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -217,7 +214,7 @@ describe('ButtplugIoDevice', () => { expect((await device.getAttribute(sensorAttrKey))?.value).toStrictEqual(Int.from(85)); }); - it('it skips sensor refresh when device has no SensorReadCmd', async () => { + it('skips sensor refresh when device has no SensorReadCmd', async () => { // Arrange const buttplugDeviceMock = mock(); @@ -235,7 +232,7 @@ describe('ButtplugIoDevice', () => { expect(buttplugDeviceMock.sensorRead).not.toHaveBeenCalled(); }); - it('it reports a refresh interval when the device has sensors', () => { + it('reports a refresh interval when the device has sensors', () => { // Arrange const buttplugDeviceMock = mock(); @@ -252,7 +249,7 @@ describe('ButtplugIoDevice', () => { expect(device.getRefreshInterval).toBe(100); }); - it('it reports no refresh interval when the device has no sensors (actuator-only)', () => { + it('reports no refresh interval when the device has no sensors (actuator-only)', () => { // Arrange const buttplugDeviceMock = mock(); @@ -267,7 +264,7 @@ describe('ButtplugIoDevice', () => { expect(device.getRefreshInterval).toBeUndefined(); }); - it('it reports no refresh interval when SensorReadCmd is an empty array', () => { + it('reports no refresh interval when SensorReadCmd is an empty array', () => { // Arrange const buttplugDeviceMock = mock(); diff --git a/tests/unit/device/protocol/estim2b/estim2bDevice.spec.ts b/tests/unit/device/protocol/estim2b/estim2bDevice.spec.ts index 0238d47d..f8af4212 100644 --- a/tests/unit/device/protocol/estim2b/estim2bDevice.spec.ts +++ b/tests/unit/device/protocol/estim2b/estim2bDevice.spec.ts @@ -93,7 +93,7 @@ describe('EStim2bDevice', () => { }); }); - it('it sets mode and updates attributes', async () => { + it('sets mode and updates attributes', async () => { // Arrange const mockProtocol = mock(); @@ -121,7 +121,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('mode'))?.value).toStrictEqual(Int.from(EStim2bMode.bounce)); }); - it('it sets channel A level and updates attributes', async () => { + it('sets channel A level and updates attributes', async () => { // Arrange const mockProtocol = mock(); @@ -147,7 +147,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('channelALevel'))?.value).toStrictEqual(Int.from(50)); }); - it('it sets channel B level and updates attributes', async () => { + it('sets channel B level and updates attributes', async () => { // Arrange const mockProtocol = mock(); @@ -173,7 +173,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('channelBLevel'))?.value).toStrictEqual(Int.from(30)); }); - it('it sets pulse frequency and updates attributes', async () => { + it('sets pulse frequency and updates attributes', async () => { // Arrange const mockProtocol = mock(); @@ -199,7 +199,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('pulseFrequency'))?.value).toStrictEqual(Int.from(75)); }); - it('it sets pulse PWM and updates attributes', async () => { + it('sets pulse PWM and updates attributes', async () => { // Arrange const mockProtocol = mock(); @@ -225,7 +225,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('pulsePwm'))?.value).toStrictEqual(Int.from(25)); }); - it('it sets high power mode to high', async () => { + it('sets high power mode to high', async () => { // Arrange const mockProtocol = mock(); @@ -251,7 +251,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('highPowerMode'))?.value).toStrictEqual(true); }); - it('it sets high power mode to low', async () => { + it('sets high power mode to low', async () => { // Arrange const mockProtocol = mock(); @@ -277,7 +277,7 @@ describe('EStim2bDevice', () => { expect((await device.getAttribute('highPowerMode'))?.value).toStrictEqual(false); }); - it('it throws when trying to set channelsJoined', async () => { + it('throws when trying to set channelsJoined', async () => { // Arrange const mockProtocol = mock(); @@ -294,7 +294,7 @@ describe('EStim2bDevice', () => { expect(mockTransport.sendAndAwaitReceive).not.toHaveBeenCalled(); }); - it('it throws when trying to set batteryStatus', async () => { + it('throws when trying to set batteryStatus', async () => { // Arrange const mockProtocol = mock(); @@ -311,7 +311,7 @@ describe('EStim2bDevice', () => { expect(mockTransport.sendAndAwaitReceive).not.toHaveBeenCalled(); }); - it('it throws when attribute does not exist on the device', async () => { + it('throws when attribute does not exist on the device', async () => { // Arrange const mockProtocol = mock(); @@ -327,7 +327,7 @@ describe('EStim2bDevice', () => { expect(mockTransport.sendAndAwaitReceive).not.toHaveBeenCalled(); }); - it('it propagates transport errors', async () => { + it('propagates transport errors', async () => { // Arrange const mockProtocol = mock(); @@ -348,7 +348,7 @@ describe('EStim2bDevice', () => { expect(mockProtocol.decode).not.toHaveBeenCalled(); }); - it('it throws when protocol decode returns an error', async () => { + it('throws when protocol decode returns an error', async () => { // Arrange const mockProtocol = mock(); @@ -369,7 +369,7 @@ describe('EStim2bDevice', () => { await expect(result).rejects.toThrow("Invalid frame for response 'bad response': unexpected byte"); }); - it('it updates attribute values on refresh', async () => { + it('updates attribute values on refresh', async () => { // Arrange const mockProtocol = mock(); diff --git a/tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts b/tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts index 0cac0998..e478e940 100644 --- a/tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts +++ b/tests/unit/device/protocol/slvCtrlPlus/genericSlvCtrlPlusDevice.spec.ts @@ -64,7 +64,7 @@ describe('GenericSlvCtrlPlusDevice', () => { } } - it('it throws an error if non-existing attribute is set', async () => { + it('throws an error if non-existing attribute is set', async () => { // Arrange const mockProtocol = mock(); @@ -84,7 +84,7 @@ describe('GenericSlvCtrlPlusDevice', () => { it.each([ { attribute: new BoolDeviceAttribute('bool', 'Bool', DeviceAttributeModifier.readWrite, undefined), valueToSet: false, protocolValue: '0' }, { attribute: new StrDeviceAttribute('str', 'String', DeviceAttributeModifier.readWrite, undefined), valueToSet: 'foo', protocolValue: 'foo' }, - ])('it sets value for $attribute.constructor.name successfully', async ({ attribute, valueToSet, protocolValue }) => { + ])('sets value for $attribute.constructor.name successfully', async ({ attribute, valueToSet, protocolValue }) => { // Arrange const mockProtocol = mock(); const mockTransport = mock(); @@ -122,7 +122,7 @@ describe('GenericSlvCtrlPlusDevice', () => { await expect(result).resolves.toStrictEqual(valueToSet); }); - it('it fails to set attribute: device reports the command as failed', async () => { + it('fails to set attribute: device reports the command as failed', async () => { // Arrange const attrName = 'bool'; @@ -155,7 +155,7 @@ describe('GenericSlvCtrlPlusDevice', () => { it.each([ [undefined], [null], - ])('it fails to set attribute: trying to set null or undefined', async (value) => { + ])('fails to set attribute: trying to set null or undefined', async (value) => { // Arrange const mockProtocol = mock(); @@ -176,7 +176,7 @@ describe('GenericSlvCtrlPlusDevice', () => { await expect(result).rejects.toThrow(`A non-null value must be set for the attribute with name '${attrName}'`); }); - it('it fails to set attribute: value is invalid for attribute type', async () => { + it('fails to set attribute: value is invalid for attribute type', async () => { // Arrange const mockProtocol = mock(); @@ -198,7 +198,7 @@ describe('GenericSlvCtrlPlusDevice', () => { await expect(result).rejects.toThrow(`Value for attribute with name '${attrName}' is not valid.`); }); - it('it updates attribute values on refresh', async () => { + it('updates attribute values on refresh', async () => { // Arrange const mockProtocol = mock(); @@ -254,7 +254,7 @@ describe('GenericSlvCtrlPlusDevice', () => { expect((await device.getAttribute('str'))?.value).toStrictEqual('hello'); }); - it('it sets attribute value to undefined when response data value is empty string', async () => { + it('sets attribute value to undefined when response data value is empty string', async () => { // Arrange const mockProtocol = mock(); @@ -296,7 +296,7 @@ describe('GenericSlvCtrlPlusDevice', () => { expect((await device.getAttribute('bool'))?.value).toBeUndefined(); }); - it('it ignores unknown attributes in refresh response', async () => { + it('ignores unknown attributes in refresh response', async () => { // Arrange const mockProtocol = mock(); diff --git a/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolLegacy.spec.ts b/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolLegacy.spec.ts index b59d1df4..cdfe69b5 100644 --- a/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolLegacy.spec.ts +++ b/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolLegacy.spec.ts @@ -48,7 +48,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.toString('utf-8')).toStrictEqual(encodedCommand); }); - it('it parses a successful device attribute response', async () => { + it('parses a successful device attribute response', async () => { // Arrange const response = "attributes;connected:ro[bool],adc:rw[bool],mode:rw[118-140],levelA:rw[int],levelB:rw[foo|bar|baz],levelC:wo[str],levelD:rw[float]"; @@ -98,7 +98,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.levelD?.modifier).toBe(DeviceAttributeModifier.readWrite); }); - it('it parses a successful device attribute response with no attributes', async () => { + it('parses a successful device attribute response with no attributes', async () => { // Arrange const response = "attributes;"; @@ -114,7 +114,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(Object.keys(result.message.data).length).toBe(0); }); - it('it ignores empty attributes', async () => { + it('ignores empty attributes', async () => { // Arrange const response = "attributes;,"; @@ -130,7 +130,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(Object.keys(result.message.data).length).toBe(0); }); - it('it ignores malformed attributes', async () => { + it('ignores malformed attributes', async () => { // Arrange const response = "attributes;foo,bar:rw[bool]"; @@ -152,7 +152,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.bar?.modifier).toBe(DeviceAttributeModifier.readWrite); }); - it('it parses successful status response', async () => { + it('parses successful status response', async () => { // Arrange const response = "status;foo:20,bar:baz,hello:"; @@ -172,7 +172,7 @@ describe('slvCtrlProtocolLegacy', () => { }); }); - it('it parses empty status response', async () => { + it('parses empty status response', async () => { // Arrange const response = "status;"; @@ -188,7 +188,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.message.data).toStrictEqual({}); }); - it('it returns an error when a mandatory segment is missing', () => { + it('returns an error when a mandatory segment is missing', () => { const protocol = new SlvCtrlProtocolLegacy(); const result = protocol.decode(Buffer.from('onlyCommandNoSemicolon')); @@ -197,7 +197,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.error).toStrictEqual({ type: 'invalid_frame', reason: 'Mandatory segment missing' }); }); - it('it parses set attribute response with value key', () => { + it('parses set attribute response with value key', () => { const protocol = new SlvCtrlProtocolLegacy(); const result = protocol.decode(Buffer.from('set-mode;42')); @@ -207,7 +207,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.message.data).toStrictEqual({ value: '42' }); }); - it('it parses introduce response with type, fw, and protocol keys', () => { + it('parses introduce response with type, fw, and protocol keys', () => { const protocol = new SlvCtrlProtocolLegacy(); const result = protocol.decode(Buffer.from('introduce;myDevice,1000,2')); @@ -217,7 +217,7 @@ describe('slvCtrlProtocolLegacy', () => { expect(result.message.data).toStrictEqual({ type: 'myDevice', fw: '1000', protocol: '2' }); }); - it('it parses the result section for error responses', () => { + it('parses the result section for error responses', () => { const protocol = new SlvCtrlProtocolLegacy(); const result = protocol.decode(Buffer.from('set-mode;42;error,Not found')); diff --git a/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolV1.spec.ts b/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolV1.spec.ts index 521ad7be..22ce33f3 100644 --- a/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolV1.spec.ts +++ b/tests/unit/device/protocol/slvCtrlPlus/slvCtrlProtocolV1.spec.ts @@ -45,7 +45,7 @@ describe('slvCtrlProtocolV1', () => { expect(result.toString('utf-8')).toStrictEqual(encodedCommand); }); - it('it parses a successful device attribute response', async () => { + it('parses a successful device attribute response', async () => { // Arrange const response = "attributes;connected:ro[bool],adc:rw[bool],mode:rw[int(118..140)],levelA:rw[int],levelB:rw[str(foo|bar|baz)],levelC:wo[str],levelD:rw[float],levelE:rw[int(1|2)];status:ok"; @@ -102,7 +102,7 @@ describe('slvCtrlProtocolV1', () => { }); - it('it throws an error if incomplete response is passed', async () => { + it('throws an error if incomplete response is passed', async () => { // Arrange const response = "status;power:20"; @@ -119,7 +119,7 @@ describe('slvCtrlProtocolV1', () => { }); }); - it('it parses a successful device attribute response with no attributes', async () => { + it('parses a successful device attribute response with no attributes', async () => { // Arrange const response = "attributes;;status:ok"; @@ -136,7 +136,7 @@ describe('slvCtrlProtocolV1', () => { expect(Object.keys(result).length).toBe(0); }); - it('it ignores empty attributes', async () => { + it('ignores empty attributes', async () => { // Arrange const response = "attributes;,;status:ok"; @@ -153,7 +153,7 @@ describe('slvCtrlProtocolV1', () => { expect(Object.keys(result).length).toBe(0); }); - it('it ignores malformed attributes', async () => { + it('ignores malformed attributes', async () => { // Arrange const response = "attributes;foo,bar:rw[bool];status:ok"; @@ -174,7 +174,7 @@ describe('slvCtrlProtocolV1', () => { expect(result.bar?.modifier).toBe(DeviceAttributeModifier.readWrite); }); - it('it parses successful status response', async () => { + it('parses successful status response', async () => { // Arrange const response = "status;foo:20,bar:baz,hello:;status:ok"; @@ -192,7 +192,7 @@ describe('slvCtrlProtocolV1', () => { }); }); - it('it parses empty status response', async () => { + it('parses empty status response', async () => { // Arrange const response = "status;;status:ok"; @@ -206,7 +206,7 @@ describe('slvCtrlProtocolV1', () => { expect(result.message.data).toStrictEqual({}); }); - it('it returns an error when the result segment has no status key', () => { + it('returns an error when the result segment has no status key', () => { const protocol = new SlvCtrlProtocolV1(); const result = protocol.decode(Buffer.from('status;foo:bar;notastatus')); diff --git a/tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts b/tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts index 4f75f891..8bec8ea7 100644 --- a/tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts +++ b/tests/unit/device/protocol/virtual/display/displayVirtualDevice.spec.ts @@ -28,7 +28,7 @@ describe('DisplayVirtualDevice', () => { ); } - it('it returns content that has been set', async () => { + it('returns content that has been set', async () => { // Arrange const device = createDevice(); @@ -37,13 +37,12 @@ describe('DisplayVirtualDevice', () => { // Act await device.setAttribute('content', content); - const result = expect((await device.getAttribute('content'))?.value); // Assert - result.toBe(content); + expect((await device.getAttribute('content'))?.value).toBe(content); }); - it('it has a refresh rate of 175ms', async () => { + it('has a refresh rate of 175ms', async () => { const device = createDevice(); expect(device.getRefreshInterval).toBe(175); diff --git a/tests/unit/device/transport/bleObserver.spec.ts b/tests/unit/device/transport/bleObserver.spec.ts index a36ee42b..8366bbad 100644 --- a/tests/unit/device/transport/bleObserver.spec.ts +++ b/tests/unit/device/transport/bleObserver.spec.ts @@ -166,8 +166,7 @@ describe('BleObserver', () => { getNobleListener('discover')?.(peripheral); - expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledOnce(); - expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledWith( + expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ type: 'ble', peripheral }), ); }); diff --git a/tests/unit/device/transport/serialPortObserver.spec.ts b/tests/unit/device/transport/serialPortObserver.spec.ts index 90b8272f..7d5e5223 100644 --- a/tests/unit/device/transport/serialPortObserver.spec.ts +++ b/tests/unit/device/transport/serialPortObserver.spec.ts @@ -110,10 +110,7 @@ describe('SerialPortObserver', () => { await observer.start(); - expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledOnce(); - expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledWith( - expect.objectContaining({ detectionId: DetectionId.create('SN001'), portInfo: port }), - ); + expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ detectionId: DetectionId.create('SN001'), portInfo: port })); }); it('generates a synthetic serial number when serialNumber is undefined', async () => { @@ -150,10 +147,7 @@ describe('SerialPortObserver', () => { await observer.start(); await observer.discoverSerialDevices(); // manually trigger a discovery run - expect(mockDeviceManager.revokeDetectedDevice).toHaveBeenCalledOnce(); - expect(mockDeviceManager.revokeDetectedDevice).toHaveBeenCalledWith( - expect.objectContaining({ detectionId: DetectionId.create('SN001') }), - ); + expect(mockDeviceManager.revokeDetectedDevice).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ detectionId: DetectionId.create('SN001') })); }); it('announces a previously revoked device again when it reappears', async () => { @@ -386,16 +380,13 @@ describe('SerialPortObserver', () => { const observer = createObserver(); await observer.start(); // first provider - full discovery, announces once - expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledOnce(); + expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ detectionId: DetectionId.create('SN001') })); await observer.start(); // second provider joins while already running, without a rescan // Re-announced unconditionally - announceDetectedDevice() itself is a no-op for a // device that's already claimed/connected, so the observer doesn't need to check first. expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledTimes(2); - expect(mockDeviceManager.announceDetectedDevice).toHaveBeenCalledWith( - expect.objectContaining({ detectionId: DetectionId.create('SN001') }), - ); }); }); }); diff --git a/tests/unit/settings/settingsManager.spec.ts b/tests/unit/settings/settingsManager.spec.ts index 8246dc57..73499796 100644 --- a/tests/unit/settings/settingsManager.spec.ts +++ b/tests/unit/settings/settingsManager.spec.ts @@ -143,10 +143,10 @@ describe('SettingsManager', () => { it('startWatching() is idempotent and stopWatching() can be called when not watching', async () => { settingsManager.load(); - settingsManager.startWatching(); - settingsManager.startWatching(); + expect(() => settingsManager.startWatching()).not.toThrow(); + expect(() => settingsManager.startWatching()).not.toThrow(); - await settingsManager.stopWatching(); - await settingsManager.stopWatching(); + await expect(settingsManager.stopWatching()).resolves.not.toThrow(); + await expect(settingsManager.stopWatching()).resolves.not.toThrow(); }); }); From da7ccf8b4caf8c243ce582b46c24228ebcf04849 Mon Sep 17 00:00:00 2001 From: HRS Date: Sun, 9 Aug 2026 18:23:36 +0200 Subject: [PATCH 2/2] Address CodeRabbit review comments on PR #109 - Assert remaining device identity, not just count, after device removal - Fix invalid resolves.not.toThrow() pattern for Promise assertions --- tests/integration/deviceEvents.spec.ts | 5 ++++- tests/unit/settings/settingsManager.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/integration/deviceEvents.spec.ts b/tests/integration/deviceEvents.spec.ts index a94d1ce9..8805bca2 100644 --- a/tests/integration/deviceEvents.spec.ts +++ b/tests/integration/deviceEvents.spec.ts @@ -122,7 +122,10 @@ describe('Device events', () => { await deviceDisconnected; - expect(deviceManager.getConnectedDevices()).toHaveLength(1); + const remainingDevices = deviceManager.getConnectedDevices(); + + expect(remainingDevices).toHaveLength(1); + expect(remainingDevices[0]?.getDeviceId).toStrictEqual(TEST_DEVICE_ID); }, 1000); it('disabling a known device closes it and re-enabling it reconnects it', async () => { diff --git a/tests/unit/settings/settingsManager.spec.ts b/tests/unit/settings/settingsManager.spec.ts index 73499796..8be3b012 100644 --- a/tests/unit/settings/settingsManager.spec.ts +++ b/tests/unit/settings/settingsManager.spec.ts @@ -146,7 +146,7 @@ describe('SettingsManager', () => { expect(() => settingsManager.startWatching()).not.toThrow(); expect(() => settingsManager.startWatching()).not.toThrow(); - await expect(settingsManager.stopWatching()).resolves.not.toThrow(); - await expect(settingsManager.stopWatching()).resolves.not.toThrow(); + await expect(settingsManager.stopWatching()).resolves.toBeUndefined(); + await expect(settingsManager.stopWatching()).resolves.toBeUndefined(); }); });