Add update-vendor.sh symlink support, unify install.sh vendor flow
- update-vendor.sh now creates webui/vendor symlinks (htm.js) - install.sh calls update-vendor.sh after package install - Add vendor/.empty and webui/vendor/.empty as directory placeholders in git
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Tests for hoover/components/applyconfirm.js
|
||||
*
|
||||
* Component-level tests: VNode structure, buildRows logic,
|
||||
* and integration behaviour. Run with `node tests/test-applyconfirm.js`.
|
||||
*/
|
||||
|
||||
import { buildRows, isPending, SUBSYSTEM_LIST } from '../webui/static/hoover/components/applyconfirm.js';
|
||||
|
||||
const SUBSYSTEM_KEYS = SUBSYSTEM_LIST.map(s => s.key);
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` \u2713 ${name}`);
|
||||
passed++;
|
||||
} catch (e) {
|
||||
console.error(` \u2717 ${name}: ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(cond, msg) {
|
||||
if (!cond) throw new Error(msg || 'Assertion failed');
|
||||
}
|
||||
|
||||
function assertEq(a, b, msg) {
|
||||
if (a !== b) throw new Error(msg || `Expected ${b}, got ${a}`);
|
||||
}
|
||||
|
||||
function assertIncludes(str, substr, msg) {
|
||||
if (!str.includes(substr)) throw new Error(msg || `Expected "${str}" to contain "${substr}"`);
|
||||
}
|
||||
|
||||
console.log('Testing ApplyConfirm component\n');
|
||||
|
||||
// === isPending ===
|
||||
test('isPending returns true for needs_apply', () => {
|
||||
assertEq(isPending({ needs_apply: true }), true);
|
||||
});
|
||||
|
||||
test('isPending returns true for pending_changes', () => {
|
||||
assertEq(isPending({ pending_changes: true }), true);
|
||||
});
|
||||
|
||||
test('isPending returns false when neither flag set', () => {
|
||||
assertEq(isPending({}), false);
|
||||
});
|
||||
|
||||
test('isPending returns false for explicit false', () => {
|
||||
assertEq(isPending({ needs_apply: false, pending_changes: false }), false);
|
||||
});
|
||||
|
||||
// === SUBSYSTEM_LIST ===
|
||||
test('SUBSYSTEM_LIST contains 5 subsystems', () => {
|
||||
assertEq(SUBSYSTEM_LIST.length, 5);
|
||||
});
|
||||
|
||||
test('SUBSYSTEM_LIST uses networkd key (not network)', () => {
|
||||
assertIncludes(SUBSYSTEM_KEYS.join(','), 'networkd', 'SUBSYSTEM_LIST should contain networkd');
|
||||
assert(SUBSYSTEM_KEYS.indexOf('network') === -1, 'SUBSYSTEM_LIST should NOT contain network');
|
||||
});
|
||||
|
||||
test('SUBSYSTEM_LIST keys match daemon response keys', () => {
|
||||
const expectedKeys = ['firewall', 'dnsmasq', 'nginx', 'wireguard', 'networkd'];
|
||||
for (const key of expectedKeys) {
|
||||
assert(SUBSYSTEM_KEYS.includes(key), `SUBSYSTEM_LIST should contain ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
// === buildRows ===
|
||||
test('buildRows returns 5 rows for empty subsystems', () => {
|
||||
const rows = buildRows({}, {});
|
||||
assertEq(rows.length, 5, 'should have 5 subsystem rows for empty state');
|
||||
});
|
||||
|
||||
test('buildRows marks pending firewall subsystem correctly', () => {
|
||||
const data = {
|
||||
firewall: {
|
||||
needs_apply: true,
|
||||
changes: [
|
||||
{ summary: 'Zone internal: interfaces changed', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const fwRow = rows[0];
|
||||
assertIncludes(fwRow.props.class, 'pending', 'firewall row should have pending class');
|
||||
});
|
||||
|
||||
test('buildRows changes are VNodes with proper structure', () => {
|
||||
const data = {
|
||||
firewall: {
|
||||
needs_apply: true,
|
||||
changes: [
|
||||
{ summary: 'Zone internal: interfaces changed', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const fwRow = rows[0];
|
||||
// Status span should contain "1 pending changes"
|
||||
const statusSpan = fwRow.ch.find(c => c.props.class && c.props.class.includes('apply-subsystem-status'));
|
||||
assert(statusSpan, 'should have status span');
|
||||
const textChild = statusSpan.ch.find(c => c.tag === '#text');
|
||||
assert(textChild && textChild.text.includes('pending changes'), 'status should contain pending changes count');
|
||||
});
|
||||
|
||||
test('buildRows marks pending dnsmasq subsystem correctly', () => {
|
||||
const data = {
|
||||
dnsmasq: {
|
||||
pending_changes: true,
|
||||
changes: [
|
||||
{ summary: 'DHCP/DNS configuration has unapplied changes', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const dnsmasqRow = rows[1];
|
||||
assertIncludes(dnsmasqRow.props.class, 'pending', 'dnsmasq row should have pending class');
|
||||
});
|
||||
|
||||
test('buildRows shows correct change count text', () => {
|
||||
const data = {
|
||||
dnsmasq: {
|
||||
pending_changes: true,
|
||||
changes: [
|
||||
{ summary: 'Range 1', detail: '' },
|
||||
{ summary: 'Range 2', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const dnsmasqRow = rows[1];
|
||||
const statusSpan = dnsmasqRow.ch.find(c => c.props.class && c.props.class.includes('apply-subsystem-status'));
|
||||
const textChild = statusSpan.ch.find(c => c.tag === '#text');
|
||||
assertEq(textChild.text, '2 pending changes');
|
||||
});
|
||||
|
||||
test('buildRows shows up-to-date for non-pending', () => {
|
||||
const data = {
|
||||
nginx: { pending_changes: false, changes: [] },
|
||||
wireguard: { pending_changes: false, changes: [] },
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const nginxRow = rows[2];
|
||||
assert(
|
||||
!nginxRow.props.class.includes('pending'),
|
||||
'nginx row should not have pending class',
|
||||
);
|
||||
const statusSpan = nginxRow.ch.find(c => c.props.class && c.props.class.includes('apply-subsystem-status'));
|
||||
const textChild = statusSpan.ch.find(c => c.tag === '#text');
|
||||
assertEq(textChild.text, 'Up to date');
|
||||
});
|
||||
|
||||
test('buildRows shows expand icon and details when expanded', () => {
|
||||
const data = {
|
||||
firewall: {
|
||||
needs_apply: true,
|
||||
changes: [
|
||||
{ summary: 'Zone internal: interfaces changed', detail: '' },
|
||||
{ summary: 'Zone dmz: services changed', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, { firewall: true });
|
||||
assertEq(rows.length, 6, 'should have 6 items (5 rows + 1 detail section)');
|
||||
});
|
||||
|
||||
test('buildRows hides expand icon when not expanded', () => {
|
||||
const data = {
|
||||
firewall: {
|
||||
needs_apply: true,
|
||||
changes: [
|
||||
{ summary: 'Zone internal: interfaces changed', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
assertEq(rows.length, 5, 'should only have 5 rows, no detail section');
|
||||
});
|
||||
|
||||
test('buildRows pending flag but no changes treated as up-to-date', () => {
|
||||
const data = {
|
||||
firewall: { needs_apply: true, changes: [] },
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const fwRow = rows[0];
|
||||
assert(
|
||||
!fwRow.props.class.includes('pending'),
|
||||
'no changes = up to date',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildRows detail section contains item VNodes', () => {
|
||||
const data = {
|
||||
firewall: {
|
||||
needs_apply: true,
|
||||
changes: [
|
||||
{ summary: 'Zone internal: interfaces changed', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, { firewall: true });
|
||||
const detailSection = rows[1];
|
||||
assertIncludes(detailSection.props.class, 'apply-detail-section', 'should be detail section');
|
||||
assert(detailSection.ch.length > 0, 'detail section should have children');
|
||||
});
|
||||
|
||||
test('buildRows handles networkd key correctly', () => {
|
||||
const data = {
|
||||
networkd: {
|
||||
pending_changes: true,
|
||||
changes: [
|
||||
{ summary: 'Network configuration has unapplied changes', detail: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
const networkdRow = rows[4]; // networkd is 5th in list
|
||||
assertIncludes(networkdRow.props.class, 'pending', 'networkd row should have pending class');
|
||||
});
|
||||
|
||||
test('buildRows row VNodes have correct tag', () => {
|
||||
const data = {
|
||||
firewall: { needs_apply: true, changes: [{ summary: 'test', detail: '' }] },
|
||||
};
|
||||
const rows = buildRows(data, {});
|
||||
for (const row of rows.slice(0, 5)) {
|
||||
assertEq(row.tag, 'div', 'row should be a div');
|
||||
assert(row.props.class.includes('apply-subsystem-row'), 'row should have apply-subsystem-row class');
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user