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 /); 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(); } });