import test from 'node:test'; import assert from 'node:assert/strict'; import { fixture } from './fixture.mjs'; import { browser } from './browser.mjs'; import { P, T, S, stub } from './helpers/projects-review-fixture.mjs'; async function setup(t, route, width=390, extra='', gateRenderer=false) { 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:'*'}]}); let rendererRequest; b.on('Fetch.requestPaused',p=>{ if(gateRenderer && p.request.url===f.base+'/render.mjs') { rendererRequest=p.requestId; return; } 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+extra}); await b.viewport(width,900);await b.navigate(f.base+'/#'+route); 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('Timed out: '+${JSON.stringify(expr)})})()`); return {b,wait,releaseRenderer:async()=>{for(let i=0;i<200&&!rendererRequest;i++)await new Promise(r=>setTimeout(r,25));assert.ok(rendererRequest,'renderer request was gated');await b.call('Fetch.continueRequest',{requestId:rendererRequest});}}; } test('conversation and room keep a mobile gutter for long human messages',async t=>{ const {b,wait}=await setup(t,'/projects/threads/'+T,390,`fixtureMessages[0].body='A very long human message '.repeat(30);fixtureRoom[0].body=fixtureMessages[0].body;`); for(const [route,selector] of [[null,'#chat-send-form'],['/projects/rooms/'+P,'#chat-room-send-form']]) { if(route)await b.evaluate(`location.hash=${JSON.stringify('#'+route)}`); await wait(`document.querySelector('${selector}')`); const result=await b.evaluate(`(()=>{const e=document.querySelector('.chat-message-human .chat-body'),m=document.querySelector('.chat-message-human .chat-message-meta');const a=e.getBoundingClientRect(),b=m.getBoundingClientRect();return {body:a.right,meta:b.right,width:document.documentElement.clientWidth}})()`); assert.ok(result.body<=result.width-10 && result.meta<=result.width-10,JSON.stringify(result)); } }); test('a repaint while the initial conversation read is pending stays in the reading state',async t=>{ const {b,wait,releaseRenderer}=await setup(t,'/projects/threads/'+T,390,`const prior=window.fetch;let gateSession=true;window.fetch=async(u,o)=>{if(gateSession && String(u)==='/api/chat/threads/${T}/sessions'){gateSession=false;await new Promise(r=>window.releaseSessions=r)}return prior(u,o)};`,true); await wait(`typeof window.releaseSessions==='function'`); await releaseRenderer(); await wait(`document.querySelector('.chat-area')`); const body=await b.evaluate(`document.querySelector('#bus-view').textContent`); assert.doesNotMatch(body,/Thread unavailable|Conversation unavailable/); await b.evaluate(`window.releaseSessions()`); await wait(`document.querySelector('#chat-send-form')`); }); test('a failed conversation read offers a retry without duplicate archive notices',async t=>{ const {b,wait}=await setup(t,'/projects/threads/'+T,390,`window.reviewFail['/api/chat/threads/${T}/sessions']=true;`); await wait(`document.querySelector('.chat-error')`); const result=await b.evaluate(`({text:document.querySelector('#bus-view').textContent,retry:!!document.querySelector('[data-chat-retry]'),notices:[...document.querySelectorAll('#bus-view [role=status]')].filter(e=>e.textContent.includes('Archive status unavailable')).length})`); assert.match(result.text,/Conversation unavailable/);assert.doesNotMatch(result.text,/Showing previously read chat content/);assert.equal(result.retry,true);assert.ok(result.notices<=1); await b.evaluate(`reviewFail['/api/chat/threads/${T}/sessions']=false;document.querySelector('[data-chat-retry]').click()`); await wait(`document.querySelector('#chat-send-form')`); }); test('a failed initial history read can be retried without losing the draft',async t=>{ const extra=`const prior=window.fetch;let failHistory=true;window.fetch=async(u,o)=>{if(failHistory && String(u).startsWith('/api/chat/threads/${T}/messages'))return new Response('{}',{status:503});return prior(u,o)};window.clearHistoryFailure=()=>{failHistory=false};`; const {b,wait}=await setup(t,'/projects/threads/'+T,390,extra); await wait(`document.querySelector('.chat-error') && document.querySelector('#chat-body')`); assert.match(await b.evaluate(`document.querySelector('.chat-log').textContent`),/Messages unavailable/); await b.evaluate(`(()=>{const e=document.querySelector('#chat-body');e.value='A draft to keep';e.dispatchEvent(new Event('input',{bubbles:true}))})()`); assert.equal(await b.evaluate(`!!document.querySelector('[data-chat-retry]')`),true); await b.evaluate(`clearHistoryFailure();document.querySelector('[data-chat-retry]').click()`); await wait(`document.querySelector('.chat-log').textContent.includes('Build a self-contained HTML Pomodoro timer')`); assert.equal(await b.evaluate(`document.querySelector('#chat-body').value`),'A draft to keep'); }); test('request identifiers are abbreviated but retain their full value',async t=>{ const id='c6946904-a20e-49c3-927f-267a67f14e81'; const {b,wait}=await setup(t,'/projects/threads/'+T,1440); await wait(`window.MosaicProjects && document.querySelector('#chat-send-form')`); const result=await b.evaluate(`(()=>{const host=document.createElement('div');host.innerHTML=MosaicProjects.requestIntro({request:${JSON.stringify(id)},revision:1,phase:'implementation'},'${P}');const e=host.querySelector('code');return {text:e.textContent,title:e.title}})()`); assert.deepEqual(result,{text:id.slice(0,8),title:id}); }); test('wide routing and data tables expose horizontal scrolling inside their panel',async t=>{ const {b,wait}=await setup(t,'/projects',1440);await wait(`document.querySelector('.chat-area')`); for(const width of [1440,390]) { await b.viewport(width,900); const r=await b.evaluate(`(()=>{const host=document.createElement('div');host.className='settings-content';host.style.width='300px';host.innerHTML='
| wide |