Files
stack/packages/webui/tests/dashboard-shell-browser.test.mjs

108 lines
9.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { w15Fixture } from './w15-fixture.mjs';
test('Dashboard routes, metrics and breadcrumbs reflect real reads and unavailable sources', {timeout:90000}, async t => {
const scenario = {
'/api/bus/agents': {rows:[{role:'coder',state:'idle'}]},
'/api/bus/inbox': {rows:[{id:'decision-1',question:'Choose a direction?'}]},
'/api/bus/messages': {rows:[],unread:2},
'/api/bus/tasks': {rows:[{task_ref:'vikunja:1/2',fields:{done:false}},{task_ref:'vikunja:1/3',fields:{done:true}}]},
'/api/chat/projects': {rows:[{id:'active',name:'Active'},{id:'archived',name:'Archived',archived:true}]},
'/api/business': {business:{id:'test-business'}},
};
const {f,b,wait}=await w15Fixture(t,{scenario});
await b.navigate(f.base+'/#/');
await wait('document.querySelector("#dashboard-kpis").textContent.includes("test") || document.querySelector("#dashboard-business").textContent.includes("test-business")');
const cells=await b.evaluate('[...document.querySelectorAll(".kpi")].map(x=>[...x.children].map(y=>y.textContent))');
assert.deepEqual(cells.map(x=>x[1]),['1','1','2','1','1','–','–']);
assert.equal(cells[5][2],'Not available yet');
assert.equal(cells[6][2],'Not available yet');
assert.match(await b.evaluate('document.querySelector("#dashboard-updated").textContent'), /^Updated \d+ s ago$/);
assert.match(await b.evaluate('document.querySelector("#dashboard-inbox-items").textContent'), /Choose a direction/);
assert.equal(await b.evaluate('document.title'), 'Dashboard · Mosaic Console');
await b.evaluate('location.hash="#/board"'); await wait('document.querySelector("#bus-view").hidden');
assert.equal(await b.evaluate('document.querySelector("#sections [aria-current]").textContent.trim()'),'Dashboard');
for (const name of ['projects','inbox','messages','tasks','agents','queue','business','docs','settings']) {
await b.evaluate(`location.hash=${JSON.stringify('#/'+name)}`);
await wait(`document.querySelector('#breadcrumbs').textContent.includes(${JSON.stringify(name[0].toUpperCase()+name.slice(1))})`);
assert.ok(await b.evaluate('!!document.querySelector("#breadcrumbs [aria-current=page]")'));
}
});
test('avatar menu supports keyboard navigation and local or SSO sign out', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t);
await b.navigate(f.base+'/#/'); await wait('!!document.querySelector("#avatar-button")');
assert.equal(await b.evaluate('document.querySelector("#avatar-name").textContent'),'Local operator');
await b.evaluate('document.querySelector("#avatar-button").focus();document.querySelector("#avatar-button").click()'); await b.key('Escape');
assert.equal(await b.evaluate('document.querySelector("#avatar-menu").hidden'),true);
await b.key('ArrowDown');
assert.equal(await b.evaluate('document.activeElement.textContent'),'Settings');
assert.equal(await b.evaluate('document.querySelector("#local-sign-out").textContent.includes("Not signed in (local dev)")'),true);
assert.equal(await b.evaluate('document.querySelectorAll("#avatar-menu select").length'),0);
await b.key('Escape'); assert.equal(await b.evaluate('document.activeElement.id'),'avatar-button');
assert.equal(await b.evaluate('document.querySelector("#avatar-menu").hidden'),true);
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`window.__scenario['/api/config']={sso:true,send:false}`});
await b.call('Page.reload'); await wait('!!document.querySelector("#sign-out") && !document.querySelector("#sign-out").hidden');
await b.evaluate('document.querySelector("#avatar-button").click()');
assert.deepEqual(await b.evaluate('(()=>{const f=document.querySelector("#sign-out");return [f.hidden,f.method,f.getAttribute("action"),document.querySelector("#local-sign-out").hidden]})()'),[false,'post','/auth/logout',true]);
assert.deepEqual(await b.evaluate('(()=>{const r=document.querySelector("#sign-out button").getBoundingClientRect();return [r.width>=44,r.height>=44]})()'),[true,true]);
});
test('compact header keeps search and theme switch usable, with refresh and pause in the avatar menu', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t);
await b.call('Emulation.setDeviceMetricsOverride',{width:390,height:900,deviceScaleFactor:1,mobile:false});
await b.navigate(f.base+'/#/'); await wait('document.querySelectorAll("#dashboard-kpis .kpi").length > 0');
const state = await b.evaluate(`(()=>{const ids=['nav-open','cmdk','theme-switch','avatar-button'];const boxes=ids.map(id=>document.getElementById(id).getBoundingClientRect());return {tops:boxes.map(x=>x.top), widths:boxes.map(x=>x.width), overflow:document.documentElement.scrollWidth>innerWidth, pauseVisible:!!document.querySelector('#pause').getClientRects().length, zeroBadges:document.querySelectorAll('#sections .count').length, held:document.querySelector('#held').textContent}})()`);
assert.ok(Math.max(...state.tops)-Math.min(...state.tops)<15, JSON.stringify(state));
assert.ok(state.widths.every(x=>x>=44)); assert.equal(state.overflow,false); assert.equal(state.pauseVisible,false);
assert.equal(state.zeroBadges,0); assert.doesNotMatch(state.held,/behind the sign-in proxy/);
await b.evaluate('document.querySelector("#cmdk").click()'); assert.equal(await b.evaluate('document.querySelector("#cmdk-dialog").open'),true);
await b.key('Escape'); await b.evaluate('document.querySelector("#avatar-button").click()');
assert.equal(await b.evaluate('!!document.querySelector("#mobile-refresh").getClientRects().length && !document.querySelector("#refresh").getClientRects().length'),true);
assert.equal(await b.evaluate('!!document.querySelector("#mobile-pause").getClientRects().length'),true);
assert.equal(await b.evaluate('(()=>{let n=0;document.querySelector("#refresh").addEventListener("click",()=>n++,{once:true});document.querySelector("#mobile-refresh").click();return n})()'),1);
await b.evaluate('document.querySelector("#mobile-pause").click()');
assert.equal(await b.evaluate('document.querySelector("#pause").getAttribute("aria-pressed")'),'true');
});
test('processing polls preserve decision focus and unchanged inbox nodes', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/bus/inbox':{rows:[{id:'decision-1',question:"Jason's direction?"}]},'/api/processing':{halted:false}}});
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:`document.addEventListener('mosaic-processing-state',()=>window.__processingReads=(window.__processingReads||0)+1)`});
await b.navigate(f.base+'/#/');
await wait('document.querySelector("#dashboard-inbox-items a") && window.__processingReads');
const reads=await b.evaluate('window.__processingReads');
await b.evaluate('window.__focusedDecision=document.querySelector("#dashboard-inbox-items a");window.__focusedDecision.focus()');
await wait(`window.__processingReads > ${reads}`);
await b.evaluate(`window.MosaicDashboard.update({inbox:[{id:'decision-1',question:"Jason's direction?"}]})`);
assert.equal(await b.evaluate('document.activeElement===window.__focusedDecision && window.__focusedDecision.isConnected'),true);
});
test('avatar closes when Tab or Shift+Tab leaves its wrapper', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t);
await b.navigate(f.base+'/#/'); await wait('!!document.querySelector("#avatar-button")');
await b.evaluate('document.querySelector("#avatar-button").focus()'); await b.key('ArrowDown');
await b.evaluate('document.querySelector("#mode").focus()'); await b.key('Tab');
assert.deepEqual(await b.evaluate('[document.querySelector("#avatar-menu").hidden,document.querySelector("#avatar-button").getAttribute("aria-expanded")]'),[true,'false']);
await b.evaluate('document.querySelector("#avatar-button").focus()'); await b.key('ArrowDown');
await b.evaluate('document.querySelector("#avatar-button").focus()'); await b.key('Tab','Tab',8);
assert.deepEqual(await b.evaluate('[document.querySelector("#avatar-menu").hidden,document.querySelector("#avatar-button").getAttribute("aria-expanded")]'),[true,'false']);
});
test('dashboard primary targets have 44px width and height', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t); await b.viewport(1440,1000); await b.navigate(f.base+'/#/');
await wait('document.querySelectorAll("#dashboard-kpis .kpi").length');
for (const width of [1440,390]) {
await b.viewport(width,900); await b.evaluate('document.querySelector("#avatar-button").click()');
const boxes=await b.evaluate(`(()=>{const selectors=['#avatar-menu a[role=menuitem]','#avatar-menu button[role=menuitem]','#dashboard-inbox > a','#show-machine'].concat(innerWidth>=760?['#processing-link']:[]);return selectors.flatMap(s=>[...document.querySelectorAll(s)].filter(e=>e.getClientRects().length).map(e=>{const r=(s==='#show-machine'?e.closest('label'):e).getBoundingClientRect();return [s,r.width,r.height]}))})()`);
assert.ok(boxes.every(([,w,h])=>w>=44&&h>=44),JSON.stringify(boxes));
await b.evaluate('document.querySelector("#avatar-button").click()');
}
});
test('Inbox reports the time of its first successful bus read', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t); await b.navigate(f.base+'/#/inbox');
await wait('document.querySelector("#bus-view h1")?.textContent === "Inbox"');
assert.doesNotMatch(await b.evaluate('document.querySelector("#bus-status").textContent'),/Read from the bus never/);
});