/** * 渐进复制尚未完成时的真实浏览器原生输入探针,由 renderer 集成 runner 注入 CDP/求值/等待端口。 * 在原卡片上双击、输入中文组合及连续字符,跨早/中/尾发布阶段检查同一 textarea 的 DOM、焦点与呈现。 * evaluate 模板字符串是在页面执行的独立脚本,保持其原字节;这里只在调用边界解释内部回调与读数。 */ /** Native input while a real progressive copy is still publishing. */ import assert from 'node:assert/strict'; // 导出给 runner:count 是原始节点规模;返回每次 input 的时间/内容/焦点样本,供外层报告或性能门槛使用。 export async function editDuringCopy({call,evaluate,wait,count}){ // 至少 1000 节点才启用并发探针,先等待首帧出现且复制还未完成,保证输入发生在真实渐进发布期间。 assert.ok(count>=1000,'concurrent input probe requires at least 1000 source nodes'); await wait('window.__canvasPerformance.read().firstPresentedFrameMs!==null','copy did not present before native input'); assert.equal(await evaluate('window.__canvasPerformance.read().localCompleteMs===null'),true,'copy finished before input probe'); // Copied cards overlap originals. Find an actually exposed point; never send a // synthetic double-click through the copied card to the original underneath. // 页面脚本先找原始 perf-0 文本标签,再按屏幕网格调用 elementFromPoint,寻找未被副本遮住的原生命中点。 const point=await evaluate(`(()=>{ const label=document.querySelector('.react-flow__node[data-id="perf-0"] [aria-label^="单击选中"]'); if(!label)throw Error('original text label is missing'); const rect=label.getBoundingClientRect(); for(let y=Math.max(0,rect.top)+3;y{ const element=document.activeElement,samples=[]; const input=()=>{ const sample={inputAt:performance.now(),value:element.value,phase:window.__copyInputProbe.phase,committed:window.__copyInputProbe.progress(),copyActive:window.__canvasPerformance.read().localCompleteMs===null}; samples.push(sample); requestAnimationFrame(()=>requestAnimationFrame(()=>{ sample.frameAt=performance.now();sample.frameDelayMs=sample.frameAt-sample.inputAt; sample.retained=element.isConnected&&document.activeElement===element; sample.presentedValue=element.value;sample.mountedNodes=document.querySelectorAll('.react-flow__node').length; })); }; element.addEventListener('input',input); window.__copyInputProbe={element,samples,phase:'early',progress(){ const status=document.querySelector('[aria-label="导入任务 1"] p[role="status"]')?.textContent??''; return Number(status.match(/·\\s*(\\d+)\\s*项/)?.[1]??0); },dispose(){element.removeEventListener('input',input);}}; })()`); // CDP 直接驱动 Chromium 的组合与插入接口,不启动操作系统输入法候选窗;用它检查编辑器对 composition 的保留。 // 先启动中文输入法组合再提交文本,随后分次插入 A/B/C;每次都等对应样本进入后续帧。 await call('Input.imeSetComposition',{text:'复',selectionStart:1,selectionEnd:1}); await call('Input.insertText',{text:'复制期间输入'}); for(const text of [' A',' B',' C']){ await call('Input.insertText',{text}); await wait(`window.__copyInputProbe.samples.at(-1)?.frameAt!==undefined`,'native input did not reach its next frame'); } // Keep the same native editor alive through later publication and ACK work. // Small scenarios can finish during the early burst; the scale gate uses 10k. // 万级场景还在 50%/90% 进度注入 M/T,保持同一编辑器不重开;测量 CDP round trip 与样本帧延迟是不同读数。 if(count>=10000)for(const [phase,fraction,text] of [['middle',.5,' M'],['tail',.9,' T']]){ await wait(`window.__copyInputProbe.progress()>=${Math.floor((count+3)*fraction)}`,'copy did not reach '+phase+' publication'); assert.equal(await evaluate('window.__canvasPerformance.read().localCompleteMs===null'),true,'copy completed before '+phase+' input'); await evaluate(`window.__copyInputProbe.phase=${JSON.stringify(phase)};true`); const started=performance.now();await call('Input.insertText',{text}); const dispatchRoundTripMs=performance.now()-started; await wait(`window.__copyInputProbe.samples.at(-1)?.phase===${JSON.stringify(phase)}&&window.__copyInputProbe.samples.at(-1)?.frameAt!==undefined`,phase+' input did not reach a frame'); await evaluate(`window.__copyInputProbe.samples.at(-1).dispatchRoundTripMs=${dispatchRoundTripMs};true`); } // 最后页面脚本读取样本与焦点状态,移除监听器、blur 并删除探针全局变量;断言使用清理前捕获的结果。 const result=await evaluate(`(()=>{ const probe=window.__copyInputProbe,result={samples:probe.samples,value:probe.element.value,retained:probe.element.isConnected&&document.activeElement===probe.element}; probe.dispose();probe.element.blur();delete window.__copyInputProbe;return result; })()`); // frameDelayMs 是 input 回调至第二次 RAF 回调的间隔,不包含 CDP 发命令前的等待,也不是像素实际呈现延迟。 // 检查同一原生输入节点/焦点留存及完整文本后缀,复制过程不能重建编辑 DOM 或覆盖用户输入。 assert.equal(result.retained,true,'copy discarded native input DOM or focus'); assert.ok(result.value.endsWith(count>=10000?'复制期间输入 A B C M T':'复制期间输入 A B C'),'native composition/typing was overwritten'); assert.ok(result.samples.some(sample=>sample.copyActive),'all native input happened after copy completion'); if(count>=10000)for(const phase of ['middle','tail'])assert.ok(result.samples.some(sample=>sample.phase===phase&&sample.copyActive),phase+' input was not observed before completion'); assert.ok(result.samples.every(sample=>sample.retained),'copy interrupted an input presentation frame'); // 每个样本挂载节点数仍少于 100,证明此探针未因原生编辑挂载全部离屏节点;这不是通用 FPS 保证。 assert.ok(result.samples.every(sample=>sample.mountedNodes<100),'native editing mounted offscreen nodes during copy'); return result; }