import { writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { fixture } from '../fixture.mjs'; import { browser } from '../browser.mjs'; export const P = '10000000-0000-4000-8000-000000000001'; export const T = '20000000-0000-4000-8000-000000000001'; export const S = '30000000-0000-4000-8000-000000000001'; export const S2 = '30000000-0000-4000-8000-000000000002'; const stub = `(() => { const real = fetch.bind(window), P=${JSON.stringify(P)}, T=${JSON.stringify(T)}, S=${JSON.stringify(S)}, S2=${JSON.stringify(S2)}; const json=(v,status=200)=>new Response(JSON.stringify(v),{status,headers:{'content-type':'application/json'}}); window.__writes=[]; window.__unexpected=[];window.__roomRows=[];window.confirm=()=>true; window.__mounts=[]; window.__contexts=[]; document.addEventListener('DOMContentLoaded', () => { window.MosaicFiles={mount:o=>{window.__mounts.push(['files',o.requestIds]);o.container.textContent='Fixture files';}}; window.MosaicProjectRemote={mount:o=>{window.__mounts.push(['remote',o.projectId,o.repositoryKey]);o.container.textContent='Fixture remote';}}; }); document.addEventListener('mosaic-project-context',e=>window.__contexts.push(e.detail)); window.fetch=async (url,init={})=>{ const path=String(url), method=init.method||'GET'; if(!path.startsWith('/api/')) return real(url,init); if(method!=='GET'){window.__writes.push({path,body:JSON.parse(init.body||'{}')}); if(window.__holdClear && path.endsWith('/clear'))return new Promise(r=>window.__releaseClear=()=>r(json({error:'fixture-refusal',sent:false},409))); if(window.__roomAccept && path.includes('/rooms/') && path.endsWith('/messages')){const id='50000000-0000-4000-8000-000000000001';window.__roomRows.push({id,ids:[id],from_role:'human',to:['alice'],body:JSON.parse(init.body).body,at:'2026-11-05T12:00:00Z'});return json({ids:[id],to:['alice']});} return json({error:'fixture-refusal',sent:false},409);} if(path==='/api/config')return json({send:true,chatGrants:[]}); if(path.startsWith('/api/bus/'))return json({rows:[],unread:0,at:'2026-11-05T12:00:00Z'}); if(path==='/api/business')return json({projects:['repo-key']}); if(path.startsWith('/api/library/status'))return json({error:'not-configured'},404); if(path==='/api/chat/projects')return json({rows:[{id:P,name:'Active project',repo:'repo-key'},{id:'10000000-0000-4000-8000-000000000002',name:'Empty project',repo:null},{id:'10000000-0000-4000-8000-000000000003',name:'Archived project',repo:null,archived:true}]}); if(path==='/api/chat/instances')return json({rows:[{instance:'alice',name:'Alice'},{instance:'bob',name:'Bob'}]}); if(path.includes('/shares'))return json({rows:[]}); if(path.endsWith('/sessions'))return json({rows:[{id:S,name:'Planning',default:true,state:'active',revision:1},{id:S2,name:'Review',state:'active',revision:1}]}); if(path.includes('/rooms/') && path.includes('/messages'))return json({rows:window.__roomRows,after:null,hasMore:false}); if(path.includes('/messages')) { if(window.__failRead)return json({error:'offline'},503); return json({rows:[{id:'40000000-0000-4000-8000-000000000001',from_role:'human',body:'A fixture message',request:'request-1',at:'2026-11-05T12:00:00Z'}],after:null,hasMore:false}); } if(path.startsWith('/api/chat/threads'))return json({rows:path.includes('shared=true')?(window.__shared?[{id:T,project:P,name:'Planning conversation',instance:'alice',owner:'Owner'}]:[]):window.__shared?[]:[{id:T,project:P,name:'Planning conversation',instance:'alice'},{id:'20000000-0000-4000-8000-000000000002',project:P,name:'alice',instance:'alice'},{id:'20000000-0000-4000-8000-000000000003',project:P,name:'bob',instance:'bob'}]}); window.__unexpected.push(path);return json({error:'unexpected-fixture-api'},403); }; })();`; export async function setup(t) { const f=await fixture(); t.after(()=>f.close()); const b=await browser({machine:false}); t.after(()=>b.close()); const errors=[]; b.on('Runtime.exceptionThrown', p=>errors.push(p.exceptionDetails.text)); 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(1440,1000); await b.navigate(f.base+'/#/chat'); const wait=expr=>b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,30));}throw new Error(${JSON.stringify(expr)});})()`); await wait('document.querySelector(".chat-projects")'); // The shell owns the canonical route dispatcher and arrives at integration. Exercise the actual // conversation renderer with canonical paths without changing the unowned baseline shell. const render=async path=>{await b.evaluate(`MosaicChat.render({path:${JSON.stringify(path)},container:document.getElementById('bus-view'),canSend:true,navigated:false})`);}; const shot=async (name,width)=>{ if(!process.env.MOSAIC_UX_SHOTS)return; await mkdir(process.env.MOSAIC_UX_SHOTS,{recursive:true});await b.viewport(width,1000);await b.evaluate('document.fonts.ready'); if (await b.evaluate('document.documentElement.scrollWidth > document.documentElement.clientWidth')) throw new Error('Document overflows at '+width); const file=join(process.env.MOSAIC_UX_SHOTS,`conversations-${name}-${width}.png`);await b.screenshot(file); const {cssContentSize:s}=await b.call('Page.getLayoutMetrics'); const {data}=await b.call('Page.captureScreenshot',{format:'png',captureBeyondViewport:true,clip:{x:0,y:0,width:Math.ceil(s.width),height:Math.ceil(s.height),scale:1}}); await writeFile(file.replace('.png','-full.png'),Buffer.from(data,'base64')); }; return {f,b,wait,render,shot,errors}; }