Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/lib/sensor.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const hueMotionPollingInterval = env.num('HUE_MOTION_POLLING_INTERVAL', 2) * 100
* accordingly. Return an EventEmitter which will be the channel over which the client is notifed of state changes.
*/
export default class Sensor extends EventEmitter {
constructor ({ bridgeIp, sensorId, username }) {
constructor ({ bridgeIp, sensorId, username, setIntervalFn }) {
super();
EventEmitter.call(this);
this._sensorId = sensorId;
Expand All @@ -23,18 +23,20 @@ export default class Sensor extends EventEmitter {
this._user = this._bridge.user(this._username);
this._lastKnownState = null;
this._lastMotionStop = null;
this._setInterval = setIntervalFn || setInterval;
}

async monitor () {
const tempSensor = await this._user.getSensor(this._sensorId);
this._lastKnownState = await tempSensor.state.presence;

setInterval(async () => {
this._setInterval(async () => {
let sensor;
try {
sensor = await this._user.getSensor(this._sensorId);
} catch (err) {
this.emit('error', err);
return;
}
const updatedState = sensor.state.presence;

Expand Down
82 changes: 60 additions & 22 deletions test/lib/sensor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ jest.mock('good-env', () => ({
if (key === 'HUE_MOTION_STOP_BUFFER') return 90;
if (key === 'HUE_MOTION_POLLING_INTERVAL') return 2;
return defaultValue;
}),
get: jest.fn().mockImplementation((key) => {
if (key === 'NODE_ENV') return 'TEST';
return undefined;
})
}));

Expand All @@ -23,9 +27,15 @@ const mockBridge = jest.fn().mockReturnValue({
user: mockUser
});

jest.mock('jshue', () => ({
bridge: mockBridge
}));
const mockBridgeFn = jest.fn().mockReturnValue({
user: mockUser
});

jest.mock('jshue', () => {
return jest.fn().mockReturnValue({
bridge: mockBridgeFn
});
});

// Mock log
jest.mock('../../src/lib/log.js', () => ({
Expand All @@ -36,24 +46,46 @@ jest.mock('../../src/lib/log.js', () => ({
import env from 'good-env';
import jsHue from 'jshue';
import log from '../../src/lib/log.js';
import Sensor from '../../src/lib/sensor.js';

describe.skip('sensor.js', () => {
describe('sensor.js', () => {
let sensor;
let Sensor;
const mockConfig = {
bridgeIp: '192.168.1.100',
sensorId: '123',
username: 'testuser'
};

beforeEach(() => {

// Synchronous setInterval mock for testing
function createManualInterval() {
let callback;
return {
setInterval: (fn, interval) => {
callback = fn;
return 1;
},
tick: async () => {
if (callback) await callback();
}
};
}

let manualInterval;

// Helper to flush all pending promises
async function flushPromises() {
for (let i = 0; i < 5; i++) {
await Promise.resolve();
await new Promise(r => setImmediate(r));
}
}

beforeEach(async () => {
jest.clearAllMocks();
jest.useFakeTimers();
sensor = new Sensor(mockConfig);
});

afterEach(() => {
jest.useRealTimers();
jest.resetModules();
Sensor = (await import('../../src/lib/sensor.js')).default;
manualInterval = createManualInterval();
sensor = new Sensor({ ...mockConfig, setIntervalFn: manualInterval.setInterval });
});

describe('constructor', () => {
Expand All @@ -66,20 +98,21 @@ describe.skip('sensor.js', () => {
});

it('should set up bridge and user correctly', () => {
expect(mockBridge).toHaveBeenCalledWith('192.168.1.100');
expect(mockBridgeFn).toHaveBeenCalledWith('192.168.1.100');
expect(mockUser).toHaveBeenCalledWith('testuser');
});
});

describe('monitor', () => {
it('should start monitoring the sensor', async () => {
const monitorPromise = sensor.monitor();
expect(sensor._lastKnownState).toBe(false);
await monitorPromise;
expect(sensor._lastKnownState).toBe(false);
expect(mockGetSensor).toHaveBeenCalledWith('123');
});

it('should emit motion_start when presence changes to true', async () => {
jest.setTimeout(10000);
const motionStartSpy = jest.fn();
sensor.on('motion_start', motionStartSpy);

Expand All @@ -94,8 +127,8 @@ describe.skip('sensor.js', () => {
});

await sensor.monitor();
jest.advanceTimersByTime(2000); // Advance past polling interval

await manualInterval.tick();
await flushPromises();
expect(motionStartSpy).toHaveBeenCalled();
});

Expand All @@ -114,21 +147,26 @@ describe.skip('sensor.js', () => {
});

await sensor.monitor();
jest.advanceTimersByTime(2000); // Advance past polling interval
jest.advanceTimersByTime(90000); // Advance past buffer period

await manualInterval.tick(); // First interval: presence changes to false
// Simulate time passage for buffer
sensor._lastMotionStop = Date.now() - 90000;
await manualInterval.tick(); // Second interval: should emit motion_stop
await flushPromises();
expect(motionStopSpy).toHaveBeenCalled();
});

it('should emit error when sensor polling fails', async () => {
const errorSpy = jest.fn();
sensor.on('error', errorSpy);

mockGetSensor.mockResolvedValueOnce({
state: { presence: false }
});
mockGetSensor.mockRejectedValueOnce(new Error('Test error'));

await sensor.monitor();
jest.advanceTimersByTime(2000); // Advance past polling interval

await manualInterval.tick();
await flushPromises();
expect(errorSpy).toHaveBeenCalledWith(expect.any(Error));
});
});
Expand Down