/** * Tests for hoover/model.js modelSet() — the WS data-streaming entry point. * * modelSet() bypasses the fetch cycle: it assigns directly to the reactive * model, clears loading unconditionally (schema defaults mean model.data is * never null), clears error, and never sets refreshing. * * model.js imports only reactivity.js — DOM-free at import, so the tests * run under plain node (same pattern as test-auth-model.js). * * Run with `node tests/test-model-set.js`. */ import { modelRegister, modelFetch, getModel, modelSet } from '../webui/static/hoover/model.js'; import { SUBSYSTEMS } from '../webui/static/hoover/schema.js'; let passed = 0; let failed = 0; const tests = []; function test(name, fn) { tests.push({ name, fn }); } function assert(cond, msg) { if (!cond) throw new Error(msg || 'Assertion failed'); } function assertEq(a, b, msg) { if (a !== b) throw new Error((msg || 'Assertion failed') + `: got ${JSON.stringify(a)}, want ${JSON.stringify(b)}`); } /** Deep equality for objects/arrays (assertEq is reference-based). */ function assertDeep(a, b, msg) { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error((msg || 'Assertion failed') + `: got ${JSON.stringify(a)}, want ${JSON.stringify(b)}`); } /* ── Tests ─────────────────────────────────────────────────── */ test('modelSet assigns data and clears loading unconditionally', () => { modelRegister('firewall', { subsystem: 'firewall', defaultData: SUBSYSTEMS.firewall.defaults, fetch: async () => ({}), }); const m = getModel('firewall'); assertEq(m.loading, true, 'registering model is loading'); assertEq(m.data, SUBSYSTEMS.firewall.defaults, 'schema defaults pre-populated'); assertEq(m.refreshing, false, 'not refreshing at rest'); modelSet('firewall', { zones: { public: {} }, pending: { pending: [] } }); assertEq(m.loading, false, 'real data ends the initial load'); assertEq(m.refreshing, false, 'modelSet never sets refreshing'); assertEq(m.error, null, 'modelSet clears error'); assertDeep(m.data.zones?.public, {}, 'data assigned to the reactive model'); }); test('modelSet is a no-op for an unregistered name', () => { assertEq(typeof modelSet('no-such-model', { x: 1 }), 'undefined', 'no throw, no return'); }); test('modelSet clears a fetch-set error and replaces error-state data', async () => { modelRegister('dnsmasq', { subsystem: 'dnsmasq', defaultData: SUBSYSTEMS.dnsmasq.defaults, fetch: async () => { throw new Error('boom'); }, }); const m = getModel('dnsmasq'); await modelFetch('dnsmasq'); assertEq(m.error, 'boom', 'fetch failure sets error'); assertEq(m.loading, false, 'fetch failure clears loading'); assertEq(m.data, SUBSYSTEMS.dnsmasq.defaults, 'failed fetch keeps schema defaults'); const delta = { leases: [{ mac: 'aa:bb', ip: '10.0.0.9' }] }; modelSet('dnsmasq', delta); assertEq(m.error, null, 'subsequent real data clears the error'); assertEq(m.data.leases?.[0]?.mac, 'aa:bb', 'delta replaces default data'); }); test('modelSet works repeatedly without flag corruption', () => { modelRegister('system', { subsystem: 'system', defaultData: SUBSYSTEMS.system.defaults, fetch: async () => ({}), }); const m = getModel('system'); modelSet('system', { load: { load1: 0.1 } }); modelSet('system', { load: { load1: 0.2 } }); assertEq(m.loading, false, 'still not loading'); assertEq(m.refreshing, false, 'still not refreshing'); assertEq(m.error, null, 'no error'); assertEq(m.data.load?.load1, 0.2, 'latest delta wins'); }); /* ── Runner ────────────────────────────────────────────────── */ (async () => { for (const { name, fn } of tests) { try { await fn(); console.log(` \u2713 ${name}`); passed++; } catch (e) { console.error(` \u2717 ${name}: ${e.message}`); failed++; } } console.log(`${passed + failed} tests: ${passed} passed, ${failed} failed`); process.exitCode = failed ? 1 : 0; })();