108 lines
9.4 KiB
JavaScript
108 lines
9.4 KiB
JavaScript
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/);
|
||
});
|