Unify Settings Layouts and Add Coverage

This commit is contained in:
2026-09-08 21:22:52 +02:00
parent 989821b7c4
commit 4a4d615c59
7 changed files with 295 additions and 43 deletions
+32 -7
View File
@@ -38,12 +38,22 @@ function browser({onlyLoader = false, withLoader = false, role = 'user', usernam
setInterval: (fn, ms) => timeout(fn, ms, true), clearInterval: id => timers.delete(id),
requestAnimationFrame: fn => timeout(fn, -1), cancelAnimationFrame: id => timers.delete(id),
location: {origin: 'https://sak.local', replace: path => redirects.push(path)}};
const context = vm.createContext({window, document: {getElementById(id) {
return nodes[id] ||= {textContent: '', value: '', checked: false, dataset: {}, classList: {toggle() {}},
setAttribute(k, v) { this[k] = v; },
getBoundingClientRect: () => ({width: 100, height: 100}),
addEventListener(k, fn) { this[k] = fn; }};
}}, Terminal, FitAddon: {FitAddon: class {
class Element {
constructor(tagName = 'div') {
this.tagName = tagName.toUpperCase(); this.children = []; this._text = '';
this.value = ''; this.checked = false; this.dataset = {}; this.classList = {toggle() {}};
}
get textContent() { return this._text + this.children.map(child => child.textContent).join(''); }
set textContent(value) { this.children.forEach(child => { child.parentNode = null; }); this.children = []; this._text = String(value); }
appendChild(child) { this.children.push(child); child.parentNode = this; return child; }
setAttribute(k, v) { this[k] = v; }
getBoundingClientRect() { return {width: 100, height: 100}; }
addEventListener(k, fn) { this[k] = fn; }
}
const context = vm.createContext({window, document: {
createElement: tag => new Element(tag),
getElementById(id) { return nodes[id] ||= new Element(); }
}, Terminal, FitAddon: {FitAddon: class {
constructor() { this.measurements = []; this.calls = 0; fits.push(this); }
proposeDimensions() { ++this.calls; return this.measurements.length ? this.measurements.shift() : {cols: 80, rows: 24}; }
}},
@@ -830,7 +840,8 @@ async function test(name, fn) { await fn(); ++passed; console.log('PASS JS:', na
});
const slotKey = index => ({index,type:'ssh-ed25519',fingerprint:'SHA256:' + String.fromCharCode(97 + index).repeat(43)});
function assertKeySlots(b, indices) {
assert.equal(b.nodes['account-keys-list'].textContent, indices.map(index => `${index}: ssh-ed25519 ${slotKey(index).fingerprint}`).join('\n'));
assert.deepEqual(b.nodes['account-keys-list'].children.map(node => [node.tagName, node.textContent]),
indices.flatMap(index => [['DT', `${index}: `], ['DD', `ssh-ed25519 ${slotKey(index).fingerprint}`]]));
assert.equal(b.nodes['account-key-index'].value, String(indices[0]));
for(let index=0;index<3;++index) {
const option=b.nodes['key-option-'+index], present=indices.includes(index);
@@ -927,6 +938,20 @@ async function test(name, fn) { await fn(); ++passed; console.log('PASS JS:', na
const s=await accountsBrowser(); s.nodes['account-public-key'].value='ssh-ed25519 AAAA'; let warning; s.window.confirm=m=>{warning=m;return true;}; s.queues[accountPath].push(failure(401)); s.click('account-key-add'); await tick();
assert.match(warning,/ALL.*web\/SSH.*401.*NOT proof/); assert.deepEqual(s.redirects,['/login']); assert.ok(s.sockets.every(s=>s.closed)); assert.doesNotMatch(s.nodes['account-operation-detail'].textContent,/completed/);
});
await test('Account definition rows and key rows clear as DOM children and fence late lists', async () => {
const b = await keyBrowser(), list = b.nodes['accounts-list'], keys = b.nodes['account-keys-list'];
assert.deepEqual(list.children.map(n => [n.tagName, n.textContent]),
[['DT', 'alice'], ['DD', 'admin (you)'], ['DT', 'carol'], ['DD', 'user']]);
assert.ok(keys.children.length > 0);
assert.ok([...list.children, ...keys.children].every(n => n.children.length === 0));
const old = [...list.children, ...keys.children], d = deferred();
b.queues['/api/settings/accounts'].push(d.promise); b.click('refresh-accounts'); await tick();
b.click('settings-serial'); await tick();
assert.equal(list.children.length, 0); assert.equal(keys.children.length, 0);
assert.ok(old.every(n => n.parentNode === null));
d.resolve(json({users: [{username: 'late', role: 'user', user_id: 9, auth_generation: 1}]})); await tick();
assert.equal(list.children.length, 0); assert.equal(keys.children.length, 0);
});
await test('Accounts list is admin-only, secret-free schema and navigation preserves both sockets', async () => {
const u = await connected(); u.click('settings-accounts'); await tick();
assert.ok(!u.calls.some(c => c.url === '/api/settings/accounts'));