diff --git a/tests/headless/renderer.integration.mjs b/tests/headless/renderer.integration.mjs index 650dfc3..7b9f649 100644 --- a/tests/headless/renderer.integration.mjs +++ b/tests/headless/renderer.integration.mjs @@ -52,7 +52,7 @@ if(performanceMode){ const boards = [root, other].map((id, i) => ({ id, workspace_id: workspace, kind: 'board', version: 1, title: i ? 'Other board' : 'Headless board' })); // 固定 PNG 只供显示 URL 返回;上传元数据和原始上传字节另外保存,不能从此图片推导所有资源都做过解码。 const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aB1sAAAAASUVORK5CYII=', 'base64'); -// 某个后续场景在前三项后挂住流,验证完整子 owner 可先写,而其他 owner 继续加载。 +// 某个后续场景在前三项后挂住流:root 两项已齐,child 只到一项,验证完整 root owner 可先写而 child 继续加载。 let scopeStreamRoot=null,releaseScopeStream;const scopeStreamGate=new Promise(resolve=>releaseScopeStream=resolve); // 分别标记错误尾部与被导航放弃的来源;abandonedGate 可令旧响应晚到。 let brokenStreamRoot=null,abandonedRoot=null,releaseAbandoned;const abandonedGate=new Promise(resolve=>releaseAbandoned=resolve); @@ -228,6 +228,7 @@ module.exports=function(source){return nativeTransform.call(this,source).replace } next=launchNext([productionMode?'start':'dev',...productionMode?[]:[`--${bundler}`],'--hostname','127.0.0.1','--port',String(port)]); // 有界探测真实代理 profile,并检查 Next 提前退出;后续页面等待还会验证应用是否实际可用。 + // 循环结束后未另设 ready 断言;120 次都未成功时仍进入浏览器阶段,由后续导航/页面等待暴露未就绪。 for (let i = 0; i < 120; i++) { if (next.exitCode !== null) throw new Error(logs); try { const r = await fetch(`http://127.0.0.1:${port}/api/foundry/profile`, { signal: AbortSignal.timeout(1000) }); if (r.ok) break; } catch {} await pause(250); } // 只保留本次隔离 Chrome 的日志尾部,CHROME_BIN 可覆盖可执行路径,不连接日常 profile。 let browserLogs=''; @@ -318,7 +319,8 @@ module.exports=function(source){return nativeTransform.call(this,source).replace await evaluate(`window.inputEvents=[];for(const type of ['pointerdown','mousedown','pointerup','mouseup','pointercancel','lostpointercapture'])window.addEventListener(type,event=>window.inputEvents.push({type,x:event.clientX,y:event.clientY,target:event.target.outerHTML?.slice(0,120)}),true);`); const rect = await evaluate('(()=>{const r=document.querySelector(".react-flow__node[data-id=b]").getBoundingClientRect();return {x:r.x+80,y:r.y+30}})()'); await call('Input.dispatchMouseEvent', { type: 'mousePressed', x: rect.x, y: rect.y, button: 'left', buttons: 1, clickCount: 1 }); - // 十帧 pointer move 只更新预览,释放时只允许一个文档请求,最终位置以 pointerup 坐标为准。 + // 连续发送十次 mouseMoved,释放后只允许新增一个文档请求,最终位置以释放坐标为准。 + // CDP 命令之间未等待 RAF,不能称作十个渲染帧;请求数断言检查整个手势的净提交。 for (let step = 1; step <= 10; step++) await call('Input.dispatchMouseEvent', { type: 'mouseMoved', x: rect.x + step * 5, y: rect.y + step * 3, button: 'left', buttons: 1 }); await call('Input.dispatchMouseEvent', { type: 'mouseReleased', x: rect.x + 50, y: rect.y + 30, button: 'left', buttons: 0 }); for (let i = 0; i < 100 && documents.get(root).elements.get('b').geometry.x === 450; i++) await pause(20); @@ -604,6 +606,7 @@ module.exports=function(source){return nativeTransform.call(this,source).replace console.log('PASS unknown types and future versions render safe read-only placeholders with custom/partition connections, preserve opaque payloads through edits and recovery, and never load opaque asset fields'); // 宿主扩展安装三个计量节点、两条自定义边和一条内置边,检查自定义端口与原始 data/path 保留。 + // __fixtureRenders 计组件函数调用,__fixtureProjectedNodes 计节点投影,__fixtureSceneBuilds 计整图回退;三个指标不能互当 DOM commit 次数。 const extensionRoot=id(810),meter={...node('meter-one',extensionRoot,350),componentId:'fixture:meter',data:{metric:3,raw:{retained:true}}},meterTwo={...meter,id:'meter-two',geometry:{...meter.geometry,x:750},data:{metric:9}},meterThree={...meter,id:'meter-three',geometry:{...meter.geometry,x:1150},data:{metric:12}},meterEdge={id:'meter-edge',incarnation:1,structure:'connection',componentId:'fixture:metric-edge',dataVersion:1,ownerCanvasId:extensionRoot,source:{elementId:meter.id,portId:'custom-out'},target:{elementId:meterTwo.id,portId:'custom-in'},data:{metric:55},path:{type:'metric-curve',retained:true}}; documents.set(extensionRoot,createDocument(extensionRoot,[{id:extensionRoot,parent:null}],[meter,meterTwo,meterThree,meterEdge,{...meterEdge,id:'unrelated-meter-edge',source:{elementId:'meter-two',portId:'custom-out'},target:{elementId:'meter-three',portId:'custom-in'}},{...meterEdge,id:'meter-builtin-edge',componentId:'connection',data:{},path:null}]));versions.set(extensionRoot,1);boards.push({id:extensionRoot,workspace_id:workspace,kind:'board',version:1,title:'Extension board'});await call('Page.navigate',{url:`http://127.0.0.1:${port}/dashboard/${workspace}/${extensionRoot}`});await until(`document.querySelectorAll('[data-renderer-fixture="meter"]').length===3&&document.querySelectorAll('[data-renderer-fixture="metric-edge"]').length===2`,'host-installed custom node/edge renderers did not mount');await until(`document.querySelectorAll('.react-flow__edge').length===3`,'built-in edge did not connect custom renderer ports');assert.equal(await evaluate(`document.querySelectorAll('[aria-label="暂不支持编辑的元素"]').length`),0);await until(`[...document.querySelectorAll('[role="status"]')].some(item=>item.textContent.trim()==='已同步')`,'extension baseline did not settle');await pause(200);await evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))');const rendersBefore=await evaluate('({...window.__fixtureRenders})'),projectsBefore=await evaluate('({...window.__fixtureProjectedNodes})');await click('Increase meter-one');for(let i=0;i<100&&documents.get(extensionRoot).elements.get('meter-one').data.metric!==4;i++)await pause(20);assert.deepEqual(documents.get(extensionRoot).elements.get('meter-one').data,{metric:4,raw:{retained:true,edited:true}});assert.deepEqual(documents.get(extensionRoot).elements.get('meter-edge'),meterEdge);await until(`[...document.querySelectorAll('[role="status"]')].some(item=>item.textContent.trim()==='已同步')`,'extension edit/ACK did not settle');await evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))');const rendersAfter=await evaluate('({...window.__fixtureRenders})');assert.ok(rendersAfter['meter-one']>rendersBefore['meter-one'],'edited custom node did not render');assert.equal(rendersAfter['meter-two'],rendersBefore['meter-two'],'unrelated custom node rendered during edit/ACK');assert.equal(rendersAfter['unrelated-meter-edge'],rendersBefore['unrelated-meter-edge'],'unrelated custom edge rendered during edit/ACK');const projectsAfter=await evaluate('({...window.__fixtureProjectedNodes})');assert.ok(projectsAfter['meter-one']>projectsBefore['meter-one']);assert.equal(projectsAfter['meter-two'],projectsBefore['meter-two'],'unrelated node projection rebuilt during edit/ACK');assert.equal(projectsAfter['meter-three'],projectsBefore['meter-three'],'unrelated node projection rebuilt during edit/ACK');console.log('PASS actual registered renderer updates the edited node while unrelated node/edge render counts remain unchanged through ACK'); await until(`window.__fixtureActualEngine?.projectId===${JSON.stringify(extensionRoot)}||window.__fixtureActualEngine?.getSnapshot().document.projectId===${JSON.stringify(extensionRoot)}`,'extension engine unavailable'); @@ -759,7 +762,7 @@ module.exports=function(source){return nativeTransform.call(this,source).replace console.log('PASS logout retires the runtime before dropping its view without a render error'); } } finally { - // 统一放开挂起响应并关闭 CDP;不依赖正在等待 gate 的请求自行退出。 + // 放开下列主流程屏障并关闭 CDP;元数据 metadataWait 不在此逐一 resolve,残留连接随后由 closeAllConnections 终止。 releaseScopeStream();releaseAbandoned();releaseFirstModel();releaseImport();releaseApply();releaseUpload(); socket?.close(); // 先请求浏览器退出,三秒仍未结束则 SIGKILL,再等待 exit 并销毁输出流。 if (chrome) { const exit = new Promise(resolve => { if (chrome.exitCode !== null) resolve(); else chrome.once('exit', resolve); }); chrome.kill(); await Promise.race([exit,pause(3000)]);if(chrome.exitCode===null)chrome.kill('SIGKILL');await exit;chrome.stdin?.destroy();chrome.stdout?.destroy();chrome.stderr?.destroy(); }