import test from 'node:test'; import assert from 'node:assert/strict'; import { setup, P, T, S, S2 } from './helpers/w15-conversations.mjs'; test('canonical project and session routes preserve identity, drafts and optional mounts', async t=>{ const {b,render,shot,wait}=await setup(t); await b.evaluate(`window.MosaicRequests={mount(){},projectSummary(o){window.__mounts.push(['summary',o.requestIds,o.project]);o.container.textContent='Fixture request summary';}}`); await render(`/projects/${P}`); await wait('document.querySelector("[data-project-content]")?.textContent.includes("Showing requests from recent messages")'); assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'),/Showing requests from recent messages/); for(const w of [1440,390]) await shot('project-overview-unloaded',w); await render(`/projects/${P}/settings`); assert.deepEqual(await b.evaluate('window.__mounts.at(-1)'),['remote',P,'repo-key']); await render(`/projects/threads/${T}/sessions/${S}`); assert.equal(await b.evaluate('document.querySelector(".chat-conversation").dataset.session'),S); assert.deepEqual(await b.evaluate('window.__contexts.at(-1)'),{projectId:P,repositoryKey:'repo-key',thread:T,session:S}); assert.ok(await b.evaluate(`!!document.querySelector('.project-tabs a[href$="/files"]') && !document.querySelector('[data-chat-files]')`)); assert.equal(await b.evaluate(`!!document.querySelector('.project-chat [data-chat-summary]') || window.__mounts.some(x=>x[0]==='summary')`),false); await b.evaluate('document.querySelector("#chat-body").value="first draft";document.querySelector("#chat-body").dispatchEvent(new Event("input",{bubbles:true}))'); for(const w of [1440,390]) await shot('conversation-progress',w); await b.evaluate('document.querySelector(".chat-details").open=true'); for(const w of [1440,390]) await shot('conversation-details',w); await render(`/projects/threads/${T}/sessions/${S2}`); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'),''); await b.evaluate('document.querySelector("#chat-body").value="second draft";document.querySelector("#chat-body").dispatchEvent(new Event("input",{bubbles:true}))'); for(const w of [1440,390]) await shot('two-named-sessions',w); await render(`/projects/threads/${T}/sessions/${S}`); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'),'first draft'); await render(`/projects/${P}`); await wait('document.querySelector("[data-project-content]")?.textContent.includes("Showing requests from recent messages")'); assert.match(await b.evaluate('document.querySelector("[data-project-content]").textContent'),/Showing requests from recent messages/); for(const w of [1440,390]) await shot('project-overview-known',w); await b.evaluate('delete window.MosaicRequests'); await render(`/projects/${P}`); await wait('document.querySelector("[data-project-content]")?.textContent.includes("Not available yet")'); await render(`/projects/threads/${T}/sessions/${S}`); await b.evaluate('window.__failRead=true');await render(`/projects/threads/${T}/sessions/${S}`); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'),'first draft'); for(const w of [1440,390]) await shot('read-failed-draft-kept',w); assert.equal(await b.evaluate('window.__writes.length'),0); }); test('room picker serializes selected instance and retains unresolved drafts', async t=>{ const {b,render,shot}=await setup(t);await render(`/projects/rooms/${P}`); assert.match(await b.evaluate('document.querySelector(".chat-room").textContent'),/Each reply stays in that member/); for(const w of [1440,390]) await shot('room-picker',w); await b.evaluate('document.querySelector("[data-room-member=bob]").click()'); assert.match(await b.evaluate('document.querySelector("#chat-room-body").value'),/^@alice /); assert.match(await b.evaluate('document.querySelector("#chat-room-tags").textContent'),/To:.*alice/); await b.evaluate('document.querySelector("#chat-room-body").value="@alice Please ask @bob too";document.querySelector("#chat-room-body").dispatchEvent(new Event("input",{bubbles:true}))'); assert.match(await b.evaluate('document.querySelector("#chat-room-tags").textContent'),/To:.*alice/); assert.equal(await b.evaluate('document.querySelector("[data-room-offer=bob]") !== null'),true); await b.evaluate('document.querySelector("[data-room-offer=bob]").click()'); assert.match(await b.evaluate('document.querySelector("#chat-room-tags").textContent'),/alice.*bob/); await b.evaluate('document.querySelector("#chat-room-body").value="@unknown message";document.querySelector("#chat-room-body").dispatchEvent(new Event("input",{bubbles:true}));document.querySelector("[data-room-member=bob]").click()'); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'),'@unknown message'); assert.match(await b.evaluate('document.querySelector("#chat-room-receipt").textContent'),/Repair/); for(const w of [1440,390]) await shot('room-invalid',w); assert.equal(await b.evaluate('window.__writes.length'),0); }); test('granted routes do not expose owner room or remote controls', async t=>{ const {b}=await setup(t); await b.evaluate(`MosaicChat.render({path:'/projects/rooms/${P}',container:document.getElementById('bus-view'),canSend:true,user:true})`); assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'),/Rooms require the business owner/); assert.equal(await b.evaluate('!!document.querySelector("#chat-room-send-form")'),false); await b.evaluate(`MosaicChat.render({path:'/projects/${P}/settings',container:document.getElementById('bus-view'),canSend:true,user:true})`); assert.equal(await b.evaluate('window.__mounts.some(x=>x[0]==="remote")'),false); assert.equal(await b.evaluate('window.__writes.length'),0); }); test('pending clear, room receipt and shared history retain truthful state', async t=>{ const {b,render,wait,shot}=await setup(t); await render(`/projects/threads/${T}/sessions/${S}`); await b.evaluate('document.querySelector(".chat-details").open=true;window.__holdClear=true;document.querySelector("#chat-clear").click()'); await wait('window.__writes.some(w=>w.path.endsWith("/clear"))'); assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'),/fixture message/); assert.match(await b.evaluate('document.querySelector("#chat-clear-receipt").textContent'),/requesting clear/); for(const w of [1440,390]) await shot('clear-pending',w); await b.evaluate('window.__releaseClear()'); await render(`/projects/rooms/${P}`); await b.evaluate('window.__roomAccept=true;document.querySelector("[data-room-member=bob]").click();const e=document.querySelector("#chat-room-body");e.value="@alice hello";e.dispatchEvent(new Event("input",{bubbles:true}));document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}))'); await wait('document.querySelector("#chat-room-receipt").textContent.includes("Accepted")'); for(const w of [1440,390]) await shot('room-sent',w); await b.evaluate('window.__shared=true'); await render(`/projects/threads/${T}`); assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'),/Read-only thread/); assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'),false); for(const w of [1440,390]) await shot('shared-readonly',w); }); test('room recipient controls accept keyboard and pointer at both widths', async t=>{ const {b,render}=await setup(t); for(const width of [1440,390]) { await b.viewport(width,1000); await render(`/projects/rooms/${P}`); await b.evaluate('document.querySelector("[data-room-chip=bob]").focus()'); await b.key(' '); assert.match(await b.evaluate('document.querySelector("#chat-room-tags").textContent'),/alice/); assert.equal(await b.evaluate('document.activeElement.dataset.roomChip'),'bob'); await b.key('Tab'); await b.key('Tab','Tab',8); await b.key('Escape'); assert.notEqual(await b.evaluate('document.activeElement.tagName'),'BODY'); const box=await b.evaluate('(()=>{const r=document.querySelector("[data-room-chip=bob]").getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()'); await b.call('Input.dispatchMouseEvent',{type:'mousePressed',x:box.x,y:box.y,button:'left',clickCount:1}); await b.call('Input.dispatchMouseEvent',{type:'mouseReleased',x:box.x,y:box.y,button:'left',clickCount:1}); assert.match(await b.evaluate('document.querySelector("#chat-room-tags").textContent'),/bob/); } });