v0.29.0
This commit is contained in:
1 parent
65e082b9d1
commit
76cb4acef0
72 files changed
+6134
-4709
No files matched your search
@@ -0,0 +1,103 @@
|
||||
#!/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.');
|
||||
Reference in new issue
Block a user