diff --git a/tests/headless/renderer-copy-input.mjs b/tests/headless/renderer-copy-input.mjs index 951ac23..c8849a2 100644 --- a/tests/headless/renderer-copy-input.mjs +++ b/tests/headless/renderer-copy-input.mjs @@ -46,6 +46,7 @@ export async function editDuringCopy({call,evaluate,wait,count}){ 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:'复制期间输入'}); @@ -70,6 +71,7 @@ export async function editDuringCopy({call,evaluate,wait,count}){ 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'); diff --git a/tests/headless/renderer-migration.mjs b/tests/headless/renderer-migration.mjs index 4a02dcb..68a5721 100644 --- a/tests/headless/renderer-migration.mjs +++ b/tests/headless/renderer-migration.mjs @@ -19,7 +19,7 @@ export function createRendererMigrationFixture({id,account,workspace,boards,docu return{states,async handle(request,path,reply,bytes){ const state=states.find(value=>path.includes('/materials/'+value.root+'/canvas'));if(!state)return false; const base=`/materials/${state.root}/canvas`,raw=bytes.length?JSON.parse(bytes.toString()):undefined; - // 未迁移时 canonical model 返回带 source_versions 的明确 migration_required;此 header 才能开启升级入口。 + // 未迁移时 canonical model 返回 409,响应正文含 migration_required 和 source_versions;不是靠响应 header 开启升级入口。 if(path.endsWith(base+'/model')&&!state.ready){reply(409,{error:'canonical_migration_required',root_id:state.root,document_format_version:1,source_versions:{[state.root]:state.version}});return true;} // 旧快照 GET 返回当前 document;PATCH 校验账户和 If-Match,并禁止 ready 后旧 writer 再写入。 if(path.endsWith(base)){ @@ -75,6 +75,7 @@ export async function verifyRendererMigration({fixture,call,evaluate,until,port, // 旧编辑器可用条件同时检查升级入口和画布非 inert,避免只凭按钮出现就认为旧写入已放开。 const editable=()=>`!!${button('升级画布')}&&!document.querySelector('.react-flow').parentElement.inert`; // 账户/workspace 级持久化迁移键,稍后比较保存的冻结 body 与服务实际收到字节。 + // 原请求尚未知时保留同一 key/body;只有 terminal not-applied 证明零写入后,显式重新准备才可以取新版本生成新请求。 const storageKey=`endoleap_canvas_migration_v1:${account}:${workspace}`; // 五份状态按下方场景依次使用,互不污染版本、回执和旧 PATCH 数量。 const[a,b,c,d,e]=fixture.states; diff --git a/tests/headless/renderer-performance.mjs b/tests/headless/renderer-performance.mjs index d0784e4..4b3c0ec 100644 --- a/tests/headless/renderer-performance.mjs +++ b/tests/headless/renderer-performance.mjs @@ -107,6 +107,7 @@ export async function measureRenderer({call,evaluate,documents,calls=[],root,par // 捕获复制开始前的原文,用来证明并发输入不进入已经捕获的副本。 const sourceText=documents.get(root).elements.get('perf-0').data.text; // 页面脚本以 rAF 采样新 DOM、进度完成、节点峰值、帧间隔,并在支持时观察 longtask;tick 反复采样,read 汇总,stop 取消并断开 observer。 + // maxMountedNodes 仅是采样帧看到的最大值;prepareMs 是从采样开始到准备提示消失的时间,未见提示时为 null,不是准备阶段独立耗时。 await evaluate(`(()=>{ const originals=new Set(['perf-source','perf-frame',...Array.from({length:${count}},(_,i)=>'perf-'+i)]),start=performance.now(),tasks=[],gaps=[]; let raf,last=start,sawPreparing=false,firstDOM=null,firstFrame=null,localComplete=null,localCompleteFrame=null,prepared=null,maxNodes=0; @@ -125,6 +126,8 @@ export async function measureRenderer({call,evaluate,documents,calls=[],root,par await evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))'); // 完成进度与已同步后再等两帧,要求 renderer 测量出现合批;统计批量大小而非只计渲染总数。 const internalsBatches=await evaluate('window.__fixtureInternalsBatches??[]');assert.ok(internalsBatches.some(size=>size>1),'mounted nodes did not share a renderer measurement batch'); + // 原文档有 count 张卡片、source、frame 和入边;副本增加 count 张卡片、frame′ 和新入边,因此总元素为 2*count+5。 + // 进度 count+3 还计入新子 canvas,canvas 记录不属于 document.elements,不能拿进度数直接推元素总量。 // 停止页面采样并验证服务元素数、复制 canvas 和上级连线。firstPresentedFrame 是检测新 DOM 后的下一次 rAF 近似值,不是 compositor 上屏证明。 const measurements=await evaluate('window.__canvasPerformance.stop()'),document=documents.get(root),copiedCanvas=[...document.canvases.values()].find(canvas=>canvas.parent&&canvas.id!==partition); assert.equal(document.elements.size,2*count+5);assert.ok(copiedCanvas);assert.ok([...document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId==='perf-source'&&element.target.elementId===copiedCanvas.parent.entryElementId));assert.ok(document.elements.has('perf-incoming'));assert.ok(measurements.firstPresentedFrameMs!==null&&measurements.localCompleteFrameMs!==null);if(count>=100)assert.ok(measurements.nodeCount