Files
stack/packages/webui/tests/browser.test.mjs
T

108 lines
8.8 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);
assert.equal(await b.evaluate('document.querySelectorAll("script").length'), 2);
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/);
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.
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'));
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(); }
});