135 lines
9.2 KiB
JavaScript
135 lines
9.2 KiB
JavaScript
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);
|
|
});
|