190 lines
6.3 KiB
JavaScript
190 lines
6.3 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
|
||
|
|
const { StatusUpdater } = require('../../src/nodered/statusUpdater');
|
||
|
|
|
||
|
|
function makeNode() {
|
||
|
|
const calls = [];
|
||
|
|
return {
|
||
|
|
calls,
|
||
|
|
status(badge) { calls.push(badge); },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeSource(initial) {
|
||
|
|
return {
|
||
|
|
badge: initial,
|
||
|
|
throwOnNext: false,
|
||
|
|
getStatusBadge() {
|
||
|
|
if (this.throwOnNext) {
|
||
|
|
this.throwOnNext = false;
|
||
|
|
throw new Error('boom');
|
||
|
|
}
|
||
|
|
return this.badge;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function makeLogger() {
|
||
|
|
const errors = [];
|
||
|
|
return {
|
||
|
|
errors,
|
||
|
|
error(msg) { errors.push(msg); },
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
test('start() schedules a tick that applies the source badge', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 1000 });
|
||
|
|
u.start();
|
||
|
|
assert.equal(node.calls.length, 0);
|
||
|
|
t.mock.timers.tick(1000);
|
||
|
|
assert.equal(node.calls.length, 1);
|
||
|
|
assert.deepEqual(node.calls[0], { fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
u.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('multiple ticks reflect the latest badge from the source', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'A' });
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 500 });
|
||
|
|
u.start();
|
||
|
|
t.mock.timers.tick(500);
|
||
|
|
source.badge = { fill: 'yellow', shape: 'dot', text: 'B' };
|
||
|
|
t.mock.timers.tick(500);
|
||
|
|
source.badge = { fill: 'red', shape: 'ring', text: 'C' };
|
||
|
|
t.mock.timers.tick(500);
|
||
|
|
assert.equal(node.calls.length, 3);
|
||
|
|
assert.equal(node.calls[0].text, 'A');
|
||
|
|
assert.equal(node.calls[1].text, 'B');
|
||
|
|
assert.equal(node.calls[2].text, 'C');
|
||
|
|
u.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('source returns null → node.status({}) is called', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource(null);
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 100 });
|
||
|
|
u.start();
|
||
|
|
t.mock.timers.tick(100);
|
||
|
|
assert.equal(node.calls.length, 1);
|
||
|
|
assert.deepEqual(node.calls[0], {});
|
||
|
|
u.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('source throw → error logged, error badge applied, next tick still runs', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const logger = makeLogger();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
source.throwOnNext = true;
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 1000, logger });
|
||
|
|
u.start();
|
||
|
|
t.mock.timers.tick(1000);
|
||
|
|
assert.equal(logger.errors.length, 1, 'error logged once');
|
||
|
|
assert.match(logger.errors[0], /boom/);
|
||
|
|
assert.deepEqual(node.calls[0], { fill: 'red', shape: 'ring', text: '⚠ boom' });
|
||
|
|
// Subsequent tick: source recovers, normal badge resumes.
|
||
|
|
t.mock.timers.tick(1000);
|
||
|
|
assert.equal(node.calls.length, 2);
|
||
|
|
assert.deepEqual(node.calls[1], { fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
u.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('stop() halts the interval AND clears the badge', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 500 });
|
||
|
|
u.start();
|
||
|
|
t.mock.timers.tick(500);
|
||
|
|
assert.equal(node.calls.length, 1);
|
||
|
|
u.stop();
|
||
|
|
assert.equal(u.isRunning, false);
|
||
|
|
// stop() pushes a clear-badge call.
|
||
|
|
assert.equal(node.calls.length, 2);
|
||
|
|
assert.deepEqual(node.calls[1], {});
|
||
|
|
// No further ticks after stop.
|
||
|
|
t.mock.timers.tick(5000);
|
||
|
|
assert.equal(node.calls.length, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('start() called twice does not schedule two intervals', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 1000 });
|
||
|
|
u.start();
|
||
|
|
u.start();
|
||
|
|
u.start();
|
||
|
|
t.mock.timers.tick(1000);
|
||
|
|
assert.equal(node.calls.length, 1, 'one tick per interval period');
|
||
|
|
t.mock.timers.tick(1000);
|
||
|
|
assert.equal(node.calls.length, 2);
|
||
|
|
u.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('intervalMs: 0 makes start() a no-op', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 0 });
|
||
|
|
u.start();
|
||
|
|
assert.equal(u.isRunning, false);
|
||
|
|
t.mock.timers.tick(10000);
|
||
|
|
assert.equal(node.calls.length, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('intervalMs omitted is also treated as a no-op', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource({ fill: 'green', shape: 'dot', text: 'OK' });
|
||
|
|
const u = new StatusUpdater({ node, source });
|
||
|
|
u.start();
|
||
|
|
assert.equal(u.isRunning, false);
|
||
|
|
t.mock.timers.tick(10000);
|
||
|
|
assert.equal(node.calls.length, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('constructor throws if node.status is missing', () => {
|
||
|
|
const source = makeSource(null);
|
||
|
|
assert.throws(
|
||
|
|
() => new StatusUpdater({ node: {}, source, intervalMs: 1000 }),
|
||
|
|
/node must expose a \.status/,
|
||
|
|
);
|
||
|
|
assert.throws(
|
||
|
|
() => new StatusUpdater({ node: null, source, intervalMs: 1000 }),
|
||
|
|
/node must expose a \.status/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('constructor throws if source.getStatusBadge is missing', () => {
|
||
|
|
const node = makeNode();
|
||
|
|
assert.throws(
|
||
|
|
() => new StatusUpdater({ node, source: {}, intervalMs: 1000 }),
|
||
|
|
/source must expose a \.getStatusBadge/,
|
||
|
|
);
|
||
|
|
assert.throws(
|
||
|
|
() => new StatusUpdater({ node, source: null, intervalMs: 1000 }),
|
||
|
|
/source must expose a \.getStatusBadge/,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isRunning getter reflects timer lifecycle', (t) => {
|
||
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
||
|
|
const node = makeNode();
|
||
|
|
const source = makeSource(null);
|
||
|
|
const u = new StatusUpdater({ node, source, intervalMs: 1000 });
|
||
|
|
assert.equal(u.isRunning, false);
|
||
|
|
u.start();
|
||
|
|
assert.equal(u.isRunning, true);
|
||
|
|
u.stop();
|
||
|
|
assert.equal(u.isRunning, false);
|
||
|
|
});
|