/** DOM contracts only; no browser layout/pixel/REA claim. See task-popconfirm-regression for temporary jsdom setup. */ import assert from 'node:assert/strict' import { createRequire } from 'node:module' const { JSDOM } = createRequire(process.cwd() + '/package.json')(process.env.YCODE_TEST_DOM_MODULE || 'jsdom') const dom = new JSDOM('
', {pretendToBeVisual:true,url:'http://localhost/'}) for(const key of ['window','document','Node','Element','HTMLElement','HTMLInputElement','MouseEvent','KeyboardEvent','Event']) Object.defineProperty(globalThis,key,{value:dom.window[key],configurable:true}) Object.defineProperty(globalThis,'navigator',{value:dom.window.navigator,configurable:true}) Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:true}) const React=require('react') as typeof import('react') const {act}=React const {createRoot}=require('react-dom/client') as typeof import('react-dom/client') const {QuestionFloating}=require('../src/renderer/src/workbuddy/conversation/QuestionFloating') as typeof import('../src/renderer/src/workbuddy/conversation/QuestionFloating') const {CompletedFold,LiveEventDisclosure}=require('../src/renderer/src/workbuddy/conversation/CompletedFold') as typeof import('../src/renderer/src/workbuddy/conversation/CompletedFold') const root=createRoot(document.getElementById('root')!) const click=(el:Element)=>el.dispatchEvent(new MouseEvent('click',{bubbles:true})) const key=(el:Element,k:string,extra={})=>el.dispatchEvent(new KeyboardEvent('keydown',{key:k,bubbles:true,cancelable:true,...extra})) const input=(value:string)=>{const el=document.querySelector('input')!; Object.getOwnPropertyDescriptor(dom.window.HTMLInputElement.prototype,'value')!.set!.call(el,value);el.dispatchEvent(new Event('input',{bubbles:true}))} let checks=0 const check=(condition:unknown,label:string)=>{assert.ok(condition,label); checks++} async function main(){ let calls=0;let fail=true;let answer:any const config={toolCallId:'q',questions:[{id:'one',question:'First',options:['A','B']},{id:'two',question:'Second',options:['X','Y'],multiSelect:true}]} await act(async()=>root.render({calls++; answer=payload;if(fail)throw Error('sync fail')}}/>)) check(document.activeElement?.getAttribute('role')==='button','first option receives focus') await act(async()=>{key(document.activeElement!,'Enter');await new Promise(r=>setTimeout(r,140))}) check(document.querySelector('[role="dialog"]')?.getAttribute('aria-label')==='Second','keyboard single choice advances page') await act(async()=>click(document.querySelectorAll('.question-floating__option')[1])) await act(async()=>input('custom answer')) await act(async()=>{click(document.querySelector('.question-floating__advance')!);click(document.querySelector('.question-floating__advance')!)}) check(calls===1,'same-tick duplicate submit locked') check(document.querySelector('[role="alert"]')?.textContent==='sync fail','sync throw visible and retryable') check(answer.questions[1].answers.join('|')==='Y|custom answer','multi-select custom supplement preserved') fail=false await act(async()=>click(document.querySelector('.question-floating__advance')!)) check(calls===2,'retry succeeds') let reject:(e:Error)=>void=()=>{};let skipCalls=0 await act(async()=>root.render({skipCalls++;return new Promise((_,r)=>{reject=r})}}/>)) await act(async()=>key(document.querySelector('[role="dialog"]')!,'Escape')) check(skipCalls===1,'Escape submits skip') check(document.querySelector('[role="dialog"]')?.getAttribute('aria-busy')==='true','skip pending busy') await act(async()=>reject(Error('async fail'))) check(document.querySelector('[role="alert"]')?.textContent==='async fail','async rejection surfaced') check(!document.querySelector('.question-floating__dismiss')?.disabled,'skip failure unlocks controls') let sends=0 await act(async()=>root.render({sends++}}/>)) await act(async()=>input('中文')) await act(async()=>key(document.querySelector('input')!,'Enter',{isComposing:true})) check(sends===0,'IME Enter does not submit') await act(async()=>key(document.querySelector('input')!,'Enter')) check(sends===1,'plain Enter submits custom answer') // A queued auto-advance from the old request must not move a replacement request. await act(async()=>root.render()) await act(async()=>click(document.querySelector('.question-floating__option')!)) await act(async()=>root.render()) await act(async()=>new Promise(r=>setTimeout(r,140))) check(document.querySelector('[role="dialog"]')?.getAttribute('aria-label')==='First','request replacement cancels old delayed advance') const parts=[{type:'reasoning',id:'r',text:'thought'},{type:'tool',toolId:'t'},{type:'text',text:'A'},{type:'tool',toolId:'later'}] as const await act(async()=>root.render(}/>)) check(document.querySelector('.cr-collapse__header')?.getAttribute('aria-expanded')==='false','completion initially collapsed') await act(async()=>key(document.querySelector('.cr-collapse__header')!,'Enter')) check([...document.querySelectorAll('[data-order]')].map(n=>n.getAttribute('data-order')).join(',')==='0,1,2,3','expanded sequence preserves original order') await act(async()=>key(document.querySelector('.cr-collapse__header')!,' ')) check(document.querySelector('.cr-collapse__header')?.getAttribute('aria-expanded')==='false','keyboard collapses completion independently') function Groups(){const [open,setOpen]=React.useState>({});return <>{['first','second'].map(id=>setOpen(previous=>({...previous,[id]:value}))}>{id} details)}} await act(async()=>root.render()) await act(async()=>click(document.querySelectorAll('.cr-collapse__header')[0])) check(document.querySelectorAll('.cr-collapse__header')[0].getAttribute('aria-expanded')==='true' && document.querySelectorAll('.cr-collapse__header')[1].getAttribute('aria-expanded')==='false','first process expansion does not expand second group') await act(async()=>click(document.querySelectorAll('.cr-collapse__header')[1])) await act(async()=>click(document.querySelectorAll('.cr-collapse__header')[0])) check(document.querySelectorAll('.cr-collapse__header')[0].getAttribute('aria-expanded')==='false' && document.querySelectorAll('.cr-collapse__header')[1].getAttribute('aria-expanded')==='true','second group retains independent state') await act(async()=>root.unmount());dom.window.close();console.log(`question-interaction: ${checks} DOM behavioral checks passed`);process.exit(0) } main().catch(error=>{console.error(error);dom.window.close();process.exit(1)})