v0.30.0
This commit is contained in:
1 parent
1431dd47f0
commit
f3dd34dae4
42 files changed
+950
-133
No files matched your search
@@ -0,0 +1,128 @@
|
||||
#!/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.');
|
||||
Reference in new issue
Block a user