Files
stack/packages/webui/tests/helpers/w15-conversations.mjs

60 lines
5.8 KiB
JavaScript

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};
}