Files
Nextcue/source/config_test.js
T

144 lines
6.2 KiB
JavaScript
Raw Normal View History

// The settings form, the upgrade script and the cue refresh (0.1.5).
//
// Seen at North Attleboro: the connection's settings couldn't be saved at all while
// it wasn't connected -- the saved decoder showed as "??" (not in the list) and a
// cue refresh that had never been saved showed as 0 (below its minimum), and either
// one blocks Save, so the login that would have fixed it couldn't be changed.
// Runs the real module code against a fake Resi; touches nothing real.
//
// node config_test.js (defaults to dist/)
// node config_test.js path/to/dist
'use strict';
const Module = require('module');
const path = require('path');
const assert = require('assert');
const dist = path.resolve(process.argv[2] || path.join(__dirname, 'dist'));
const stubs = {
'@companion-module/base': {
InstanceBase: class { updateStatus() {} log() {} setVariableValues() {} },
InstanceStatus: { Ok: 'ok', Connecting: 'c', ConnectionFailure: 'f', BadConfig: 'b' },
runEntrypoint() {}, combineRgb() { return 0 }, Regex: {},
},
'./variables.js': { UpdateVariableDefinitions() {} },
'./actions.js': { UpdateActions() {} },
'./feedbacks.js': { UpdateFeedbacks() {} },
'./presets.js': { UpdatePresets() {} },
};
const realLoad = Module._load;
Module._load = function (req, parent, isMain) {
if (stubs[req]) return stubs[req];
return realLoad.apply(this, arguments);
};
let fetches = [];
let cues = [{ uuid: 'a', name: 'Welcome', position: '00:05:00' }, { uuid: 'b', name: 'Message', position: '00:30:00' }];
let cueStatus = 200;
const reply = (status, body) => ({
ok: status >= 200 && status < 300, status,
headers: { get: () => null, getSetCookie: () => [] },
json: async () => body, text: async () => JSON.stringify(body),
});
global.fetch = async (url) => {
const p = String(url).replace('https://central.resi.io', '').split('?')[0];
fetches.push(p);
if (/\/events\/[^/]+$/.test(p)) return reply(200, { eventProfileId: 'P1', startTime: '2026-10-07T13:30:00Z' });
if (p.endsWith('/cues')) return reply(cueStatus, cues);
return reply(404, {});
};
const { ModuleInstance } = require(path.join(dist, 'main.js'));
const { UpgradeScripts } = require(path.join(dist, 'upgrades.js'));
function fresh() {
const inst = Object.create(ModuleInstance.prototype);
inst.config = {};
inst.secrets = { password: 'pw' };
inst.venues = [];
inst.playersByVenueUuid = {};
inst.updateStatus = () => {};
inst.setVariableValues = () => {};
inst.infos = [];
inst.warns = [];
inst.log = (lvl, m) => { (lvl === 'info' ? inst.infos : inst.warns).push(m); };
return inst;
}
let passed = 0;
async function test(name, fn) { await fn(); passed++; console.log(` ok ${name}`); }
(async () => {
await test('the saved decoder is a valid choice even before Resi answers', () => {
const inst = fresh();
inst.config = { defaultPlayerHardwareId: 'DEC1' };
const field = inst.getConfigFields().find((f) => f.id === 'defaultPlayerHardwareId');
const ids = field.choices.map((c) => c.id);
assert.ok(ids.includes('DEC1'), `choices were ${JSON.stringify(ids)}`);
assert.match(field.choices.find((c) => c.id === 'DEC1').label, /not connected/);
});
await test('once connected, the decoder is listed once, under its real name', () => {
const inst = fresh();
inst.config = { defaultPlayerHardwareId: 'DEC1' };
inst.venues = [{ uuid: 'V1', userName: 'North Attleboro' }];
inst.playersByVenueUuid = { V1: [{ playerHardwareId: 'DEC1', userName: 'North Attleboro' }] };
const field = inst.getConfigFields().find((f) => f.id === 'defaultPlayerHardwareId');
const dec = field.choices.filter((c) => c.id === 'DEC1');
assert.strictEqual(dec.length, 1);
assert.match(dec[0].label, /North Attleboro/);
});
await test('the upgrade fills settings an older connection never saved', () => {
const r = UpgradeScripts[0]({}, { config: { username: 'x', playerPollMs: 3000 }, actions: [], feedbacks: [] });
assert.ok(r.updatedConfig, 'config should be updated');
assert.strictEqual(r.updatedConfig.cuePollMs, 5000);
assert.strictEqual(r.updatedConfig.listPollMs, 120000);
assert.strictEqual(r.updatedConfig.positionTrimMs, 0);
assert.strictEqual(r.updatedConfig.playerPollMs, 3000, 'a saved value is kept');
});
await test('the upgrade leaves a complete connection alone', () => {
const r = UpgradeScripts[0]({}, {
config: { playerPollMs: 3000, positionTrimMs: 0, cuePollMs: 5000, listPollMs: 120000 }, actions: [], feedbacks: [],
});
assert.strictEqual(r.updatedConfig, null);
});
await test('event details are read once per event, and the cue log only speaks on a change', async () => {
const inst = fresh();
inst.customerId = 'C';
inst.currentEventId = 'EV1';
inst.cookieHeader = 'livingas1=T';
inst.lastLoginMs = Date.now();
fetches = [];
for (let i = 0; i < 5; i++) await inst.refreshEventProfileAndCues();
assert.strictEqual(fetches.filter((p) => p.endsWith('/events/EV1')).length, 1, 'event detail once');
assert.strictEqual(fetches.filter((p) => p.endsWith('/cues')).length, 5, 'cues every refresh');
assert.strictEqual(inst.infos.filter((m) => /Loaded/.test(m)).length, 1, `logged ${inst.infos.length} times`);
cues = cues.concat({ uuid: 'c', name: 'Closing', position: '00:45:00' });
await inst.refreshEventProfileAndCues();
assert.strictEqual(inst.infos.filter((m) => /Loaded/.test(m)).length, 2, 'a changed list is logged');
inst.currentEventId = 'EV2';
await inst.refreshEventProfileAndCues();
assert.strictEqual(fetches.filter((p) => p.endsWith('/events/EV2')).length, 1, 'a new event is read');
});
await test('a rate-limited cue request holds off every Resi call', async () => {
const inst = fresh();
inst.customerId = 'C';
inst.currentEventId = 'EV1';
inst.cookieHeader = 'livingas1=T';
inst.lastLoginMs = Date.now();
inst.config = { username: 'a@b.c' };
cueStatus = 429;
await assert.rejects(inst.refreshEventProfileAndCues(), /cues failed \(429\)/);
await assert.rejects(inst.ensureAuthenticated(false), /rate-limiting.*retrying in \d+s/);
cueStatus = 200;
});
console.log(`\n${passed} tests passed`);
})().catch((e) => { console.error(e); process.exit(1); });