92 lines
2.9 KiB
JavaScript
92 lines
2.9 KiB
JavaScript
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
const EventEmitter = require('events');
|
||
|
|
|
||
|
|
const { bindStateEvents, isOperationalState, OPERATIONAL_STATES } =
|
||
|
|
require('../../src/state/stateBindings');
|
||
|
|
|
||
|
|
function makeFakeState() {
|
||
|
|
const emitter = new EventEmitter();
|
||
|
|
let current = 'idle';
|
||
|
|
return {
|
||
|
|
emitter,
|
||
|
|
setState(s) { current = s; },
|
||
|
|
getCurrentState() { return current; },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
test('bindStateEvents attaches both listeners and they fire on emit', () => {
|
||
|
|
const state = makeFakeState();
|
||
|
|
let posCalls = 0;
|
||
|
|
let stateCalls = 0;
|
||
|
|
let lastStateArg = null;
|
||
|
|
|
||
|
|
bindStateEvents({
|
||
|
|
state,
|
||
|
|
onPositionChange: () => { posCalls++; },
|
||
|
|
onStateChange: (newState) => { stateCalls++; lastStateArg = newState; },
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(state.emitter.listenerCount('positionChange'), 1);
|
||
|
|
assert.equal(state.emitter.listenerCount('stateChange'), 1);
|
||
|
|
|
||
|
|
state.emitter.emit('positionChange', 42);
|
||
|
|
state.emitter.emit('stateChange', 'operational');
|
||
|
|
|
||
|
|
assert.equal(posCalls, 1);
|
||
|
|
assert.equal(stateCalls, 1);
|
||
|
|
assert.equal(lastStateArg, 'operational');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('bindStateEvents teardown removes both listeners and is idempotent', () => {
|
||
|
|
const state = makeFakeState();
|
||
|
|
const teardown = bindStateEvents({
|
||
|
|
state,
|
||
|
|
onPositionChange: () => {},
|
||
|
|
onStateChange: () => {},
|
||
|
|
});
|
||
|
|
|
||
|
|
assert.equal(state.emitter.listenerCount('positionChange'), 1);
|
||
|
|
assert.equal(state.emitter.listenerCount('stateChange'), 1);
|
||
|
|
|
||
|
|
teardown();
|
||
|
|
assert.equal(state.emitter.listenerCount('positionChange'), 0);
|
||
|
|
assert.equal(state.emitter.listenerCount('stateChange'), 0);
|
||
|
|
|
||
|
|
teardown();
|
||
|
|
assert.equal(state.emitter.listenerCount('positionChange'), 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('bindStateEvents validates context shape', () => {
|
||
|
|
assert.throws(() => bindStateEvents(null), /ctx\.state\.emitter is required/);
|
||
|
|
assert.throws(
|
||
|
|
() => bindStateEvents({ state: makeFakeState() }),
|
||
|
|
/handlers are required/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isOperationalState returns true for operational/accelerating/decelerating/warmingup', () => {
|
||
|
|
const state = makeFakeState();
|
||
|
|
for (const s of ['operational', 'accelerating', 'decelerating', 'warmingup']) {
|
||
|
|
state.setState(s);
|
||
|
|
assert.equal(isOperationalState(state), true, `expected ${s} to be operational`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isOperationalState returns false for non-operational states and bad input', () => {
|
||
|
|
const state = makeFakeState();
|
||
|
|
for (const s of ['idle', 'starting', 'stopping', 'coolingdown', 'emergencystopped']) {
|
||
|
|
state.setState(s);
|
||
|
|
assert.equal(isOperationalState(state), false, `expected ${s} not to be operational`);
|
||
|
|
}
|
||
|
|
assert.equal(isOperationalState(null), false);
|
||
|
|
assert.equal(isOperationalState({}), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('OPERATIONAL_STATES list is exported and frozen-ish (no extras beyond contract)', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
[...OPERATIONAL_STATES].sort(),
|
||
|
|
['accelerating', 'decelerating', 'operational', 'warmingup'],
|
||
|
|
);
|
||
|
|
});
|