108 lines
8.8 KiB
JavaScript
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(); }
|
|
});
|