import test from 'node:test'; import assert from 'node:assert/strict'; import { fixture } from './fixture.mjs'; import { browser } from './browser.mjs'; import { P, T, stub } from './helpers/projects-review-fixture.mjs'; async function setup(t, path='/projects/'+P, width=1440) { const f=await fixture(), b=await browser({machine:false}); t.after(async()=>{await b.close();await f.close();}); await b.call('Fetch.enable',{patterns:[{urlPattern:'*'}]}); b.on('Fetch.requestPaused',p=>void b.call(p.request.url.startsWith(f.base+'/')?'Fetch.continueRequest':'Fetch.failRequest',p.request.url.startsWith(f.base+'/')?{requestId:p.requestId}:{requestId:p.requestId,errorReason:'BlockedByClient'}).catch(()=>{})); await b.call('Page.addScriptToEvaluateOnNewDocument',{source:stub}); await b.viewport(width,900); await b.navigate(f.base+'/#'+path); const run=fn=>b.evaluate('('+fn.toString()+')()'); const wait=expr=>b.evaluate('(async()=>{for(let i=0;i<200;i++){if('+expr+')return;await new Promise(r=>setTimeout(r,25))}throw Error('+JSON.stringify(expr)+')})()'); const render=path=>{const canonical=path==='/projects'?'/chat':/^\/projects\/(threads|rooms)\//.test(path)?'/chat'+path.slice('/projects'.length):/^\/projects\/[^/]+$/.test(path)?'/chat/projects/'+path.slice('/projects/'.length):path;return b.evaluate('MosaicChat.render({path:'+JSON.stringify(canonical)+',container:document.querySelector("#bus-view"),canSend:true,navigated:false})');}; await wait('document.querySelector(".project-header")'); await run(async()=>{await new Promise(r=>setTimeout(r,150));}); return {f,b,run,wait,render}; } test('probe: room recipient completion still works after an unchanged refresh',async t=>{ const {run,render,wait}=await setup(t,'/projects/rooms/'+P); await wait('document.querySelector("#chat-room-body") && !document.querySelector("#chat-room-body").disabled'); await run(()=>{ window.roomField=document.querySelector('#chat-room-body');roomField.focus();roomField.value='@a';roomField.setSelectionRange(2,2);roomField.dispatchEvent(new Event('input',{bubbles:true})); }); await wait('!document.querySelector("#chat-room-completion").hidden && !!document.querySelector("[data-room-complete=assistant]")'); await render('/projects/rooms/'+P); const result=await run(()=>{ const menu=document.querySelector('#chat-room-completion'); const before={menuOpen:!menu.hidden,value:document.querySelector('#chat-room-body').value,same:roomField.isConnected}; document.querySelector('[data-room-complete=assistant]').click(); return {...before,after:document.querySelector('#chat-room-body').value}; }); console.log('ROOM_COMPLETION',JSON.stringify(result)); assert.equal(result.menuOpen,true);assert.equal(result.after,'@assistant ','the preserved completion menu must accept a recipient'); }); test('probe: invisible request revision does not collapse requirement details or drop focus',async t=>{ const {run,render,wait}=await setup(t,'/projects/'+P+'/requirements'); await wait('document.querySelector("[data-project-content] .project-columns section details summary")'); await run(()=>{ window.requirement=document.querySelector('[data-project-content] .project-columns section details'); requirement.open=true;window.requirementSummary=requirement.querySelector('summary');requirementSummary.focus(); window.beforeText=requirement.textContent; reviewStates['request-1']={...reviewStates['request-1'],revision:2}; const now=Date.now();Date.now=()=>now+11000; }); await render('/projects/'+P+'/requirements'); await run(async()=>{await new Promise(r=>setTimeout(r,200));}); const result=await run(()=>{ const detail=document.querySelector('[data-project-content] .project-columns section details'); return {oldConnected:requirement.isConnected,open:detail.open,focused:document.activeElement===detail.querySelector('summary'),textSame:detail.textContent===beforeText}; }); console.log('REQUIREMENT_REFRESH',JSON.stringify(result)); assert.equal(result.textSame,true); assert.equal(result.open&&result.focused,true,'an unchanged requirement must keep its disclosure and focus'); }); test('probe: focused row survives a keyed move',async t=>{ const {run,render}=await setup(t,'/projects/'+P+'/conversations'); await run(()=>{ window.movingRow=document.querySelectorAll('[data-conversation-id]')[2];movingRow.focus(); fixtureThreads.unshift(fixtureThreads.pop()); }); await render('/projects/'+P+'/conversations'); const result=await run(()=>({connected:movingRow.isConnected,focused:document.activeElement===movingRow,active:document.activeElement.tagName,first:document.querySelector('[data-conversation-id]')===movingRow})); console.log('FOCUSED_MOVE',JSON.stringify(result)); assert.equal(result.connected&&result.first,true); assert.equal(result.focused,true,'moving a keyed conversation must keep its keyboard focus'); }); test('probe: stale project-content read cannot roll back newer request state',async t=>{ const {run,render,wait}=await setup(t); await wait('document.querySelector(".project-work .project-chip")?.textContent==="Building"'); await run(()=>{ const now=Date.now();window.clockOffset=11000;Date.now=()=>now+clockOffset; const old=window.fetch;window.holdOld=true; window.fetch=async(url,init)=>{ const res=await old(url,init); if(String(url)==='/api/requests/request-1'&&holdOld){holdOld=false;return new Promise(resolve=>window.releaseOld=()=>resolve(res));} return res; }; }); await render('/projects/'+P); await wait('!!window.releaseOld'); await run(()=>{reviewStates['request-1']={...reviewStates['request-1'],phase:'review',revision:2,mode:'supervised'};clockOffset+=11000;}); await render('/projects/'+P); await wait('document.querySelector(".project-work .project-chip")?.textContent==="In review"'); await run(async()=>{window.newerText=document.querySelector('[data-project-content]').textContent;releaseOld();await new Promise(r=>setTimeout(r,200));}); const result=await run(()=>({newer:newerText,final:document.querySelector('.project-work .project-chip').textContent,content:document.querySelector('[data-project-content]').textContent,context:document.querySelector('[data-project-context]').textContent})); console.log('CONTENT_RACE',JSON.stringify(result)); assert.equal(result.final,'In review','old project content must not overwrite the newer request phase'); assert.equal(result.content,result.newer,'no project content may roll back after the old read'); assert.match(result.content,/Supervised/); }); for (const changed of [false, true]) for (const method of ['click', 'keyboard']) { test(`room completion ${method} works after ${changed ? 'message-only' : 'unchanged'} refresh`, async t => { const {b,run,render,wait}=await setup(t,'/projects/rooms/'+P); await wait('document.querySelector("#chat-room-body") && !document.querySelector("#chat-room-body").disabled'); await run(()=>{const f=document.querySelector('#chat-room-body');f.focus();f.value='@a';f.setSelectionRange(2,2);f.dispatchEvent(new Event('input',{bubbles:true}));}); if(changed) await run(()=>fixtureRoom.push({id:'50000000-0000-4000-8000-000000000004',ids:['50000000-0000-4000-8000-000000000004'],from_role:'assistant',to:['human'],body:'New message',at:'2026-11-06T10:26:00Z'})); await render('/projects/rooms/'+P); if(changed) await wait('document.querySelectorAll("[data-message]").length===4'); if(method==='click') await run(()=>document.querySelector('[data-room-complete=assistant]').click()); else await b.key('Enter'); assert.equal(await run(()=>document.querySelector('#chat-room-body').value),'@assistant '); }); } test('changed requirement content retains the disclosure and focused summary',async t=>{ const {run,render,wait}=await setup(t,'/projects/'+P+'/requirements'); await wait('document.querySelector("[data-project-content] .project-columns section details summary")'); await run(()=>{const d=document.querySelector('[data-project-content] .project-columns section details');d.open=true;d.querySelector('summary').focus();reviewDrafts['request-1'].requirements[0].text='Updated requirement text';const now=Date.now();Date.now=()=>now+11000;}); await render('/projects/'+P+'/requirements'); await wait('document.querySelector("[data-project-content]")?.textContent.includes("Updated requirement text")'); const result=await run(()=>{const d=document.querySelector('[data-project-content] .project-columns section details');return {open:d.open,focused:document.activeElement===d.querySelector('summary')}}); assert.deepEqual(result,{open:true,focused:true}); }); test('closed room on an organization Console offers no unusable open action', async t => { const {b,render,wait}=await setup(t,'/projects/rooms/'+P); await b.evaluate(`window.MosaicOrg={id:'alpha',route:()=>'/projects/rooms/${P}'};window.reviewClosed=true`); await render('/projects/rooms/'+P); await wait(`document.querySelector('.chat-room')`); const result=await b.evaluate(`({text:document.querySelector('.chat-area').innerText,open:!!document.querySelector('[data-room-open-inline],#chat-room-open')})`); assert.match(result.text,/Room changes are unavailable on this Console\./); assert.doesNotMatch(result.text,/Open the room before sending/); assert.equal(result.open,false); });