The org dashboard lists the installed system-type modules with their own labels and descriptions, marks which are enabled, and gives the owner an enable or disable control through plugins.set behind the owner fence, with the revision and the transaction receipt. The project page selects from the organization's enabled set through project.system-types.set and shows a disabled selection as inactive, as system-types.console reports. Both routes go through the per-org Console call path and its admission checks. docs/plans/2026-11-10_system-type-toolsets-design.md proposes how a launch turns a project's active system types into toolsets under the role, mission and task ceilings. Design only; no roles/ or contract change is applied.
127 lines
12 KiB
JavaScript
127 lines
12 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { browser } from './browser.mjs';
|
|
import { fixture } from './fixture.mjs';
|
|
|
|
test('served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures', { timeout: 120000 }, async () => {
|
|
const f = await fixture(), b = await browser();
|
|
const out = process.env.WEBUI_EVIDENCE;
|
|
if (out) mkdirSync(out, { recursive: true });
|
|
const wait = expr => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,50))}throw new Error('Condition timed out')})()`);
|
|
try {
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: 'window.errors=[];addEventListener("error",e=>errors.push(e.message));addEventListener("unhandledrejection",e=>errors.push(String(e.reason)));' });
|
|
await b.viewport(1440); await b.navigate(f.base); await wait('document.querySelectorAll("table.sessions tbody tr").length===3');
|
|
assert.equal(await b.evaluate('document.querySelector("#waiting-count").textContent'), '2');
|
|
assert.equal(await b.evaluate('document.querySelectorAll("#projects [data-project]").length'), 3);
|
|
assert.equal(await b.evaluate('document.querySelector("#session-count").textContent'), '3 of 4');
|
|
await b.evaluate('document.querySelector("#hide-offline").click()');
|
|
assert.equal(await b.evaluate('document.querySelectorAll("table.sessions tbody tr").length'), 4);
|
|
// Rows 129, 154, 191, 194 and 294: chat.js, editor.js, processing.js, agents.js and federation.js are the scripts added, and #1839 adds requests.js (the intake panel under an owner's chat message), all same origin; the list is the pin, so another script is a visible change. #797 adds project-archive.js immediately after chat.js. F2 adds onboarding.js after org-create.js, and F3 adds console-restart.js after it. U2 (#1873) adds workspace-data.js and workspace.js before console-restart.js, and U4b adds tasks-board.js after workspace.js. U5 (#1873) adds panes.js (Side by side) just before bus.js, which mounts it. P2 (#1873) adds system-types.js after project-remote.js.
|
|
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("script")].map(s => s.getAttribute("src"))'), ['/editor.js', '/processing.js', '/org-create.js', '/onboarding.js', '/workspace-data.js', '/workspace.js', '/tasks-board.js', '/console-restart.js', '/display.js', '/inbox.js', '/files.js', '/project-remote.js', '/system-types.js', '/federation.js', '/brand.js', '/app.js', '/markdown-preview.js', '/projects.js', '/chat.js', '/project-archive.js', '/agents.js', '/panes.js', '/bus.js', '/requests.js']);
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('script[src="/processing.js"]').length`), 1);
|
|
assert.equal(await b.evaluate(`document.querySelectorAll('script[src="/chat.js"]').length`), 1);
|
|
assert.equal(await b.evaluate('!!window.injected'), false);
|
|
// Project filtering does not hide another project's waiting requests.
|
|
await b.evaluate('document.querySelector("[data-project=proj]").click()');
|
|
assert.equal(await b.evaluate('document.querySelectorAll("table.sessions tbody tr").length'), 2);
|
|
assert.equal(await b.evaluate('document.querySelector("#waiting-count").textContent'), '2');
|
|
await b.evaluate('[...document.querySelectorAll("[data-project]")].find(e=>e.dataset.project==="").click()');
|
|
await b.evaluate('document.querySelector("table.sessions [data-open]").focus()');
|
|
const first = await b.evaluate('document.activeElement.dataset.open');
|
|
await b.key('ArrowDown'); assert.notEqual(await b.evaluate('document.activeElement.dataset.open'), first);
|
|
// Use the registered row regardless of scanner ordering.
|
|
await b.evaluate('[...document.querySelectorAll("table.sessions [data-open]")].find(e=>e.dataset.open==="proj/agent1").focus()');
|
|
await b.key('Enter');
|
|
assert.equal(await b.evaluate('document.activeElement.id'), 'inspector-title');
|
|
assert.equal(await b.evaluate('document.querySelector("#inspector").hidden'), false);
|
|
assert.match(await b.evaluate('document.querySelector("#inspection").textContent'), /Registered fixture task/);
|
|
// #1511: attribution shows for the registered task only, as a claim, and only where the task is the registered one.
|
|
assert.match(await b.evaluate('document.querySelector("#inspection").textContent'), /Task set by\s*fixture-setter \(as claimed by the caller, not verified\)/);
|
|
assert.match(await b.evaluate('document.querySelector("table.sessions tr[data-row=\\"proj/agent1\\"] td.task").textContent'), /set by fixture-setter/);
|
|
assert.equal(await b.evaluate('[...document.querySelectorAll("table.sessions td.task")].filter(td=>td.textContent.includes("set by")).length'), 1, 'transcript-derived rows carry no attribution');
|
|
assert.match(await b.evaluate('document.querySelector("#waiting .wait-item p:nth-of-type(2)").textContent'), /set by fixture-setter|Build the fixture task/);
|
|
// R1-U1: a transcript-derived task is "not applicable", never "unknown", and never borrows the registered setter.
|
|
await b.evaluate('[...document.querySelectorAll("table.sessions [data-open]")].find(e=>e.dataset.open!=="proj/agent1").focus()'); await b.key('Enter');
|
|
await wait('!document.querySelector("#inspection").textContent.includes("Registered fixture task")');
|
|
assert.match(await b.evaluate('document.querySelector("#inspection").textContent'), /Task set by\s*not applicable \(task is not from a registration\)/);
|
|
assert.equal(await b.evaluate('document.querySelector("#inspection").textContent.includes("fixture-setter")'), false);
|
|
assert.doesNotMatch(await b.evaluate('document.querySelector("#inspection").textContent'), /Task set by\s*unknown/);
|
|
await b.evaluate('[...document.querySelectorAll("table.sessions [data-open]")].find(e=>e.dataset.open==="proj/agent1").focus()'); await b.key('Enter');
|
|
await wait('document.querySelector("#inspection").textContent.includes("Registered fixture task")');
|
|
assert.equal(await b.evaluate('!!window.injected'), false);
|
|
await b.evaluate('document.querySelector("#reply").focus()');
|
|
await b.call('Input.insertText', { text: 'keep this draft' });
|
|
await b.evaluate('document.querySelector("#reply").setSelectionRange(3,7);document.querySelector("#refresh").click()');
|
|
await wait('!document.querySelector("#refresh").disabled');
|
|
assert.deepEqual(await b.evaluate('({value:document.querySelector("#reply").value,start:document.querySelector("#reply").selectionStart,end:document.querySelector("#reply").selectionEnd,focus:document.activeElement.id})'), { value: 'keep this draft', start: 3, end: 7, focus: 'reply' });
|
|
// Real proxy to real board, with only its transport stubbed.
|
|
await b.evaluate('document.querySelector(".reply-form").requestSubmit()');
|
|
await wait('document.querySelector(".receipt")?.textContent.startsWith("delivered")');
|
|
assert.equal(f.captures.length, 1); assert.equal(await b.evaluate('document.querySelector("#reply").value'), '');
|
|
f.failReply();
|
|
await b.evaluate('document.querySelector("#reply").focus()'); await b.call('Input.insertText', { text: 'failed draft' });
|
|
await b.evaluate('document.querySelector(".reply-form").requestSubmit()');
|
|
await wait('document.querySelector(".receipt")?.textContent.includes("fixture refusal")');
|
|
assert.equal(await b.evaluate('document.querySelector("#reply").value'), 'failed draft');
|
|
assert.match(await b.evaluate('document.querySelector(".receipt").textContent'), /failed \(exit 4\): fixture refusal <unsafe>/);
|
|
await b.evaluate('document.querySelector("#inspection [data-seen]").click()');
|
|
await wait('document.querySelector("#seen-count").textContent==="1"');
|
|
assert.equal(await b.evaluate('document.querySelector("#waiting-count").textContent'), '1');
|
|
await b.evaluate('document.querySelector("#inspection [data-seen]").click()');
|
|
await wait('document.querySelector("#seen-count").textContent==="0"');
|
|
await b.evaluate('document.querySelector("#close").focus()'); await b.key('Escape');
|
|
assert.equal(await b.evaluate('document.querySelector("#inspector").hidden'), true);
|
|
assert.equal(await b.evaluate('document.activeElement.dataset.open'), 'proj/agent1');
|
|
if (out) {
|
|
await b.screenshot(join(out, 'console-harbor-light-1440.png'));
|
|
await b.viewport(320, 1000);
|
|
await b.screenshot(join(out, 'console-harbor-light-320.png'));
|
|
await b.viewport(1440);
|
|
}
|
|
// All supported palette/mode tokens and rendered badge/text backgrounds. The ten palettes are installed theme modules (S4): wait for the
|
|
// list, and fail a palette that did not apply, so a missing option cannot leave the page on Harbor and pass.
|
|
await wait('window.MosaicAppearance.themes()?.length===10');
|
|
const contrast = await b.evaluate(`(() => {
|
|
const failures=[]; let count=0,lowest=100;
|
|
const rgb = c => { const a=c.match(/[\\d.]+/g).map(Number);return c.startsWith('color(')?[a[0]*255,a[1]*255,a[2]*255,a[3]??1]:[a[0],a[1],a[2],a[3]??1] };
|
|
const blend=(f,g)=>[...f.slice(0,3).map((v,i)=>v*f[3]+g[i]*(1-f[3])),1];
|
|
const lum=c=>c.slice(0,3).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4}).reduce((s,v,i)=>s+v*[.2126,.7152,.0722][i],0);
|
|
for(const p of Brand.palettes)for(const m of ['light','dim','dark']){
|
|
document.querySelector('#palette').value=p.id;document.querySelector('#palette').dispatchEvent(new Event('change'));
|
|
document.querySelector('#mode').value=m;document.querySelector('#mode').dispatchEvent(new Event('change'));
|
|
if(document.documentElement.dataset.palette!==p.id||document.documentElement.dataset.mode!==m)failures.push('not applied '+p.id+'/'+m);
|
|
for(const s of ['h1','.muted','.source','.badge-ok','.badge-attn','.badge-danger','.badge-muted','.count','th','.session-open','.btn']){
|
|
const e=document.querySelector(s);if(!e)continue; const layers=[];for(let n=e;n;n=n.parentElement)layers.push(rgb(getComputedStyle(n).backgroundColor));
|
|
let bg=[255,255,255,1];while(layers.length)bg=blend(layers.pop(),bg);
|
|
const style=getComputedStyle(e), fg=blend(rgb(style.color),bg),a=lum(fg),b=lum(bg),ratio=(Math.max(a,b)+.05)/(Math.min(a,b)+.05);
|
|
const large=parseFloat(style.fontSize)>=24 || (parseFloat(style.fontSize)>=18.66&&parseInt(style.fontWeight)>=700);
|
|
count++;lowest=Math.min(lowest,ratio);if(ratio<(large?3:4.5))failures.push(p.id+'/'+m+' '+s+' '+ratio.toFixed(2));
|
|
}
|
|
}return {failures,count,lowest};
|
|
})()`);
|
|
assert.deepEqual(contrast.failures, []); assert.ok(contrast.count >= 300); console.log('Rendered contrast:', JSON.stringify(contrast));
|
|
for (const width of [320, 390, 768, 1440, 2560]) {
|
|
await b.viewport(width, 1000);
|
|
assert.equal(await b.evaluate('document.documentElement.scrollWidth<=document.documentElement.clientWidth'), true, `${width} page overflow`);
|
|
await b.evaluate('[...document.querySelectorAll("table.sessions [data-open]")].find(e=>e.dataset.open==="proj/agent1").click()');
|
|
assert.equal(await b.evaluate('document.documentElement.scrollWidth<=document.documentElement.clientWidth'), true, `${width} inspector overflow`);
|
|
assert.equal(await b.evaluate('document.querySelector("#reply").getBoundingClientRect().right<=innerWidth'), true);
|
|
if (out && [320, 1440].includes(width)) await b.screenshot(join(out, `console-inspector-${width}.png`));
|
|
await b.evaluate('document.querySelector("#close").click()');
|
|
}
|
|
// Empty and unreachable pages are tested as browser responses, never live data claims.
|
|
// Stop only the fixture board. The page keeps the last data and reports its URL.
|
|
await new Promise(r => f.board.close(r));
|
|
await b.evaluate('document.querySelector("#refresh").click()');
|
|
await wait('!document.querySelector("#error").hidden');
|
|
const error = await b.evaluate('document.querySelector("#error").textContent');
|
|
assert.ok(error.includes(f.boardURL)); assert.match(error, /Showing last known data/);
|
|
assert.ok(await b.evaluate('document.querySelectorAll("table.sessions tbody tr").length') > 0);
|
|
assert.deepEqual(await b.evaluate('window.errors'), []);
|
|
await b.navigate(f.base); await wait('!document.querySelector("#error").hidden');
|
|
assert.match(await b.evaluate('document.querySelector("#error").textContent'), /No board data loaded/);
|
|
} finally { await b.close(); await f.close(); }
|
|
});
|