// 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); });