#!/usr/bin/env node // Simulated vhost DOM: selector eligibility, independent edits and lifecycle. // This is not a browser rendering test or a Rust health-probe test. import fs from 'node:fs'; import vm from 'node:vm'; const core = fs.readFileSync('src/admin/web/vhosts/core.js', 'utf8'); const health = fs.readFileSync('src/admin/web/vhosts/health.js', 'utf8'); const editor = fs.readFileSync('src/admin/web/vhosts/editor.js', 'utf8'); const render = fs.readFileSync('src/admin/web/vhosts/render.js', 'utf8'); const first = core.indexOf(' function memberRow('); const last = core.indexOf(' function countryRateEditor(', first); const assert = (condition, message) => { if (!condition) throw new Error(message); }; assert(first >= 0 && last > first, 'Vhost member editor missing'); class Element { constructor(tag = 'div', value = '', css = '') { this.tag = tag; this.value = String(value ?? ''); this.textContent = this.value; this.children = []; this.parent = null; this.name = ''; this.checked = false; this.disabled = false; this.hidden = false; this.listeners = new Map(); this.classes = new Set(css.split(/\s+/).filter(Boolean)); } append(...children) { for (const child of children) { if (child.parent) child.remove(); this.children.push(child); child.parent = this; } } replaceChildren(...children) { for (const child of [...this.children]) child.remove(); this.append(...children); } remove() { if (!this.parent) return; this.parent.children.splice(this.parent.children.indexOf(this), 1); this.parent = null; } addEventListener(type, callback) { this.listeners.set(type, callback); } dispatch(type) { this.listeners.get(type)?.(); } querySelectorAll(selector) { const result = []; const walk = parent => { for (const child of parent.children) { if ((selector === 'input,select' && ['input', 'select'].includes(child.tag)) || (selector === '*' || selector === undefined)) result.push(child); walk(child); }}; walk(this); return result; } querySelector(selector) { const match = /^\[name="([^"]+)"\]$/.exec(selector); return match ? this.querySelectorAll('*').find(node => node.name === match[1]) : null; } } const el = (tag, value, css) => new Element(tag, value, css); const text = (tag, key, css) => el(tag, key, css); const field = (parent, name, key, value, type = 'text') => { const wrapper = el('label'); const input = el(type === 'select' ? 'select' : 'input', value); input.name = name; input.checked = type === 'checkbox' && !!value; wrapper.append(input); parent.append(wrapper); return input; }; const button = (key, callback) => { const node = el('button', key); node.addEventListener('click', callback); return node; }; const note = (parent, key) => { const node = text('p', key); parent.append(node); return node; }; const context = { el, text, field, button, note, t: key => key, str: (name, scope) => scope.querySelector(`[name="${name}"]`).value.trim(), toast: () => { throw Error('Unexpected error toast'); }, changed: () => {}, appConfirm: async () => true, adminListenerBackendCandidate: () => false, backendHealthEditorControl: null, read: (name, scope) => scope ? scope.querySelector(`[name="${name}"]`) : ({ "health-mode": "http", "health-path": "/shared", "health-host": "shared.example", interval: "8500", timeout: "1800", slow: "600", fall: "4", rise: "3", "status-min": "201", "status-max": "398" }[name] != null ? { value: ({ "health-mode": "http", "health-path": "/shared", "health-host": "shared.example", interval: "8500", timeout: "1800", slow: "600", fall: "4", rise: "3", "status-min": "201", "status-max": "398" })[name] } : null), }; const exports = vm.runInNewContext(`${core.slice(first, last)}\n${health}\n({ memberRow, backendHealthEditor })`, context); const members = el('div'); const probe = (mode, path) => ({ mode, path, host: null, interval_ms: 5000, timeout_ms: 1000, slow_response_ms: 500, fall: 2, rise: 1, expected_status_min: 200, expected_status_max: 399 }); const a = { id: 'alpha', url: 'http://192.0.2.1:9000', health_check_enabled: true, health_check: probe('http', '/a') }; const b = { id: 'disabled', url: 'http://192.0.2.2:9000', health_check_enabled: false, health_check: probe('http', '/b') }; const c = { id: 'charlie', url: 'http://192.0.2.3:9000', health_check_enabled: true, health_check: null }; for (const backend of [a, b, c]) exports.memberRow(members, { weight: 1, ...backend }); const [rowA, rowB, rowC] = members.children; const wrapper = el('div'); context.backendHealthEditorControl = exports.backendHealthEditor(wrapper, members); const select = wrapper.querySelector('[name="health-backend-select"]'); const control = (row, name) => row.memberHealthPanel.querySelector(`[name="${name}"]`); assert(select.children.length === 2 && select.children[0].textContent.includes('alpha') && select.children[1].textContent.includes('charlie'), 'Unmonitored backend listed in selector'); assert(!rowA.memberHealthPanel.hidden && rowB.memberHealthPanel.hidden && rowC.memberHealthPanel.hidden, 'Selected panel visibility incorrect'); assert(control(rowB, 'member-health-path').disabled, 'Unmonitored override must be non-editable'); assert(!control(rowA, 'member-health-path').disabled, 'Monitored override unexpectedly disabled'); assert(control(rowA, 'member-health-path').value === '/a', 'Existing per-member override not loaded'); select.value = '1'; select.dispatch('change'); assert(rowA.memberHealthPanel.hidden && !rowC.memberHealthPanel.hidden, 'Switch did not change selected panel'); const customC = control(rowC, 'member-health-custom'); customC.checked = true; customC.dispatch('change'); assert(control(rowC, 'member-health-mode').value === 'http' && control(rowC, 'member-health-interval').value === '8500' && control(rowC, 'member-health-path').value === '/shared', 'First override did not inherit shared settings'); const pathC = control(rowC, 'member-health-path'); pathC.value = '/charlie-ready'; customC.checked = false; customC.dispatch('change'); customC.checked = true; customC.dispatch('change'); assert(pathC.value === '/charlie-ready', 'Toggling custom override discarded edits'); assert(!pathC.disabled, 'New custom override should become editable'); select.value = '0'; select.dispatch('change'); assert(control(rowA, 'member-health-path').value === '/a' && pathC.value === '/charlie-ready', 'Switch discarded unsaved member edits'); select.value = '1'; select.dispatch('change'); assert(pathC.value === '/charlie-ready' && !rowC.memberHealthPanel.hidden, 'Returning to backend lost its edits'); rowC.querySelector('[name="member-id"]').value = 'renamed'; rowC.querySelector('[name="member-id"]').dispatch('input'); assert(select.children[1].textContent.includes('renamed'), 'Backend rename not reflected in selector'); const monitorA = rowA.querySelector('[name="member-health-enabled"]'); monitorA.checked = false; monitorA.dispatch('change'); assert(select.children.length === 1 && select.children[0].textContent.includes('renamed'), 'Disabled monitor remains in selector'); assert(control(rowA, 'member-health-path').value === '/a' && control(rowA, 'member-health-path').disabled, 'Disabling monitor lost override or left active controls'); monitorA.checked = true; monitorA.dispatch('change'); assert(select.children.length === 2 && pathC.value === '/charlie-ready', 'Re-enabling monitor did not restore choice/edits'); const removeC = rowC.children[0].children[1]; // backend-card header's Remove button removeC.dispatch('click'); assert(members.children.length === 2 && select.children.length === 1 && !rowA.memberHealthPanel.hidden, 'Deleting selected backend did not refresh selector'); assert(wrapper.querySelectorAll('*').includes(rowA.memberHealthPanel) && !wrapper.querySelectorAll('*').includes(rowC.memberHealthPanel), 'Removed backend panel remained retained in DOM'); monitorA.checked = false; monitorA.dispatch('change'); assert(select.disabled && select.children.length === 0 && rowB.memberHealthPanel.hidden, 'Empty monitored backend list not handled'); assert(editor.includes('health_check: yes("member-health-custom", row.memberHealthPanel)'), 'Per-member health settings not collected from selected panels'); assert(editor.includes('backendHealthEditorControl = backendHealthEditor(g, members)'), 'Selector not wired to Health tab'); assert(render.includes('backendHealthEditorControl?.reveal('), 'Invalid hidden member settings cannot be revealed'); assert(core.includes('backendHealthEditorControl = state.backendHealthEditorControl || null;') && core.includes('pagesEditor, backendHealthEditorControl,'), 'Draft switching lost selected backend editor'); assert(fs.readFileSync('src/admin/router.rs','utf8').includes('include_str!("web/vhosts/health.js")'), 'Bundled health editor missing'); console.log('PASS: 0.30.0 monitored-only backend selector, independent unsaved edits, monitor toggle, rename/removal, empty list and validation routing.');