#!/usr/bin/env node // Exercise the real vhost WWW-routing editor code with a small DOM stand-in. // This is a browser-UI state test, not a Rust request-routing test. import fs from 'node:fs'; import vm from 'node:vm'; const editor = fs.readFileSync('src/admin/web/vhosts/editor.js', 'utf8'); const first = editor.indexOf(' const wwwPanel = el("section", undefined, "vh-wide vh-www-panel");'); const last = editor.indexOf(' field(g, "forward-client-ip",', first); if (first === -1 || last === -1) throw new Error('WWW editor component missing'); const source = editor.slice(first, last); if (editor.indexOf('readHosts = hostTagEditor(g, value.frontend?.hosts') < editor.indexOf('g = group("vhFrontends")')) { throw new Error('Host chips must be in the same Frontends tab as WWW routing'); } class ClassList { constructor() { this.names = new Set(); } add(name) { this.names.add(name); } toggle(name, active) { if (active) this.names.add(name); else this.names.delete(name); } contains(name) { return this.names.has(name); } } class Element { constructor(tag = 'div', text = '', css = '') { this.tag = tag; this.textContent = text || ''; this.classList = new ClassList(); if (css) for (const name of css.split(' ')) this.classList.add(name); this.children = []; this.hidden = false; this.checked = false; this.value = ''; this.attrs = {}; this.listeners = new Map(); } append(...children) { for (const child of children) { child.parent = this; this.children.push(child); } } prepend(...children) { for (const child of [...children].reverse()) { child.parent = this; this.children.unshift(child); } } replaceChildren(...children) { this.children = []; this.append(...children); } closest(tag) { let node = this.parent; while (node && node.tag !== tag) node = node.parent; return node; } setAttribute(key, value) { this.attrs[key] = String(value); } addEventListener(event, listener) { this.listeners.set(event, listener); } dispatch(event) { this.listeners.get(event)?.(); } } const el = (tag, content, css) => new Element(tag, content, css); const t = key => key === 'vhWwwSharedCount' ? '{count} names / one vhost' : key; const text = (tag, key, css) => el(tag, t(key), css); const g = new Element(); const hosts = new Element('textarea'); hosts.value = 'suba.local\nsubb.local\n*.local'; const read = name => { if (name !== 'hosts') throw new Error(name); return hosts; }; const lines = raw => raw.split(/[\n,]+/).map(s => s.trim()).filter(Boolean); let changes = 0; const changed = () => { changes++; }; const value = { www_redirect: false, www_redirect_target: 'subb.local' }; const document = { createElement: tag => new Element(tag) }; const field = (parent, name, key, initial, type) => { const label = new Element('label'); const input = new Element('input'); input.name = name; input.type = type; input.checked = Boolean(initial); label.append(input, text('span', key)); parent.append(label); return input; }; let refreshCanonical = () => {}; const context = { el, text, field, g, t, value, read, lines, changed, document, refreshCanonical }; const component = vm.runInNewContext(`(() => { ${source}\nreturn { wwwPanel, wwwRedirect, wwwTarget, wwwChoices, wwwDetails, wwwInactive, wwwOther, wwwNoHosts, wwwHostCount, syncCanonical }; })()`, context); const check = (condition, message) => { if (!condition) throw new Error(message); }; const choice = (baseIndex, directionIndex) => component.wwwChoices.children[baseIndex].children[1].children[directionIndex]; const route = (baseIndex, directionIndex) => choice(baseIndex, directionIndex).children[1].children[1].textContent; check(!component.wwwDetails.hidden && !component.wwwInactive.hidden, 'Direction preview should be visible even when redirect is disabled'); check(component.wwwTarget.value === 'subb.local', 'Saved canonical domain was lost'); check(component.wwwChoices.children.length === 2, 'Wildcard must not become a canonical pair'); check(component.wwwHostCount.textContent === '3 names / one vhost', 'Shared host count not updated'); check(route(0, 0) === 'www.suba.local → suba.local', 'Incorrect www -> non-www preview'); check(route(0, 1) === 'suba.local → www.suba.local', 'Incorrect non-www -> www preview'); check(route(1, 0) === 'www.subb.local → subb.local', 'Incorrect second domain preview'); check(choice(1, 0).classList.contains('is-selected'), 'Saved target not visibly selected'); component.wwwRedirect.checked = true; component.wwwRedirect.dispatch('change'); check(!component.wwwDetails.hidden && component.wwwInactive.hidden, 'Switch did not reveal WWW routing choices'); check(component.wwwRedirect.attrs['aria-expanded'] === 'true', 'Toggle accessibility state not updated'); check(component.wwwOther.children.some(child => child.textContent === 'suba.local'), 'Non-canonical alias must remain available'); check(component.wwwOther.children.some(child => child.textContent === '*.local'), 'Wildcard alias must remain available'); choice(0, 1).children[0].checked = true; choice(0, 1).children[0].dispatch('change'); check(component.wwwTarget.value === 'www.suba.local', 'WWW destination selected without a separately declared www host was lost'); check(choice(0, 1).classList.contains('is-selected'), 'Selected direction not styled'); check(changes === 2, 'Toggle and radio change did not mark editor dirty'); hosts.value = 'subb.local\nwww.subb.local\n*.local'; context.refreshCanonical(); check(component.wwwTarget.value === 'subb.local', 'Deleting selected domain did not choose a valid fallback'); check(component.wwwChoices.children.length === 1, 'Duplicate base and www domains should be one pair'); check(route(0, 1) === 'subb.local → www.subb.local', 'Wrong preview for canonical www host'); choice(0, 1).children[0].checked = true; choice(0, 1).children[0].dispatch('change'); check(component.wwwTarget.value === 'www.subb.local', 'Explicit WWW target not serialized'); check(component.wwwOther.children.every(child => child.textContent !== 'www.subb.local'), 'Redirected companion must not appear as a non-redirected alias'); hosts.value = '*.local'; context.refreshCanonical(); check(component.wwwNoHosts.hidden === false && component.wwwTarget.value === '', 'Wildcard-only configurations need a useful empty state'); check(component.wwwRedirect.disabled && !component.wwwRedirect.checked, 'Wildcard-only configuration must disable redirect'); console.log('PASS: WWW-routing UI two-way choices, implicit www target, shared aliases, dynamic host edits, wildcard exclusion and empty state.');