From 357c92d7492ebd0226e54ea69a6ebd48f0c17089 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 18:23:15 +0800 Subject: [PATCH] docs(tests): clarify legacy canvas DOM scenario coverage --- tests/canvas-member-scenario.mjs | 4 +++- tests/canvas-ui-check.mjs | 8 +++++--- tests/canvas-upload-scenario.mjs | 4 +++- tests/canvas-v2-scenario.mjs | 5 +++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/canvas-member-scenario.mjs b/tests/canvas-member-scenario.mjs index 1bccb57..aca962f 100644 --- a/tests/canvas-member-scenario.mjs +++ b/tests/canvas-member-scenario.mjs @@ -25,6 +25,7 @@ export async function canvasMemberScenario({root,act,h,useCanvasTree,makeCanvasN // 内存 API 按路径分流:完整快照保存、版本命令、分区创建与成员迁移,各自记录参与者版本。 mock(async(url,options={})=>{ const parts=new URL(url,'http://localhost').pathname.split('/'),id=parts[6],suffix=parts.slice(8).join('/'),method=options.method||'GET'; + // 此分支未区分 method,POST 也直接得到空列表;下方同路径 POST 分支不会到达,不模拟 canvas_version 推进。 if(suffix==='ai/runs')return json({runs:[]}); // 慢响应延迟超过两秒;一次网络失败只影响首个请求,允许检查重新探测而非永久判为不支持。 if(suffix==='capabilities'){ @@ -126,7 +127,8 @@ export async function canvasMemberScenario({root,act,h,useCanvasTree,makeCanvasN await act(async()=>{await tree.undo();await tick();});assert.ok(tree.rootDocument.nodes.some(n=>n.id===pendingImage.id));assert.ok(!tree.current.current.get(child).nodes.some(n=>n.id===pendingImage.id)); await act(async()=>{await tree.redo();await tick();});assert.ok(tree.current.current.get(child).nodes.some(n=>n.id===pendingImage.id));assert.equal(posts.length,2,'Undo/redo after persistence use real ownership transactions'); console.log('PASS pending image ownership: editable local moves, atomic local undo, current-owner upload resolution and transactional undo after persistence'); - // 版本刷新和分区创建响应都不能覆盖本地 pending/已完成图片;重复父快照也只能采用一次。 + // 空 AI 响应与分区创建响应不能覆盖本地 pending/已完成图片;重复父快照也只能采用一次。 + // 当前 mock 的 ai/runs POST 被前面的通用分支截获,因此该调用不能单独证明版本推进后的 refresh 合并行为。 await reset('create-delayed'); const asyncImage=makeCanvasNode('image',{x:800,y:500},'blob:async-upload');asyncImage.id='async-image';asyncImage.pendingUpload=asyncImage.id; await act(async()=>{tree.dispatch({type:'nodes',changes:[{type:'add',item:asyncImage}]});await tick();}); diff --git a/tests/canvas-ui-check.mjs b/tests/canvas-ui-check.mjs index 560ecc4..8f5ae84 100644 --- a/tests/canvas-ui-check.mjs +++ b/tests/canvas-ui-check.mjs @@ -19,7 +19,7 @@ const { build, stop } = tools("esbuild"); // 独立 DOM 和 localhost origin 提供 localStorage 等环境,不请求 localhost 服务。 const dom = new JSDOM('
', { url: "http://localhost", pretendToBeVisual: true }); const { window } = dom; -// React's async act creates MessageChannels; track and close test-owned ports. +// 记录此进程后来创建的 MessageChannel;React 异步 act 可持有这些端口,最终关闭以免测试进程悬挂。 const NativeMessageChannel = globalThis.MessageChannel; const channels = []; globalThis.MessageChannel = class extends NativeMessageChannel { constructor() { super(); channels.push(this); } }; @@ -109,7 +109,8 @@ try { let calls = 0, controls, renders = 0; apiMock = async () => { calls++; return Response.json({ runs: [] }); }; const tree = { busy: false, writers: { current: new Map() } }; - // 挂载 AI hook 并计数渲染,检查空结果轮询既不重复请求也不造成额外渲染。 + // 挂载 AI hook 并计数 Probe 函数调用,检查截止时间前触发的空闲回调不请求也不增加调用次数。 + // 这是函数执行计数,不是 React DOM commit 次数;未推进墙上时间便手动触发 timer,验证的是 deadline 门禁。 function Probe() { renders++; controls = useCanvasAI("w", "root", true, tree); return null; } try { await act(async () => { root.render(h(Probe)); await tick(); }); @@ -257,7 +258,8 @@ try { const { feedbackScenario } = await import("./feedback-scenario.mjs"); await feedbackScenario({ root, act, h, AppFeedback, IconButton, notify, dismissNotification, GraphCanvas, window, makeCanvasNode, emptyCanvas, event, tick }); await act(async () => { root.unmount(); await tick(); }); -// 清理临时打包文件、JSDOM、esbuild 及 act 创建的 MessageChannel 端口,恢复原通道构造器。 +// 断言失败也会进入以下清理;root.unmount 只在正常路径执行,finally 仍关闭 JSDOM、esbuild 和记录到的通道。 +// 清理临时打包文件并恢复原通道构造器,不逐项恢复仅供此独立进程使用的 DOM 全局替身。 } finally { await rm(folder, { recursive: true, force: true }); window.close(); stop(); for (const channel of channels) { channel.port1.close(); channel.port2.close(); } diff --git a/tests/canvas-upload-scenario.mjs b/tests/canvas-upload-scenario.mjs index d643b8d..2d6889a 100644 --- a/tests/canvas-upload-scenario.mjs +++ b/tests/canvas-upload-scenario.mjs @@ -75,11 +75,13 @@ export async function canvasUploadScenario({root,act,h,GraphCanvas,window,emptyC assert.ok(document.querySelector(`img[src="${localURL}"]`),'Local image remains visible while remote preview loads'); const remote=document.querySelector('.react-flow__node img[src$="?variant=preview"]');await act(async()=>{remote.dispatchEvent(new window.Event('load'));await tick();}); assert.ok(!document.querySelector(`img[src="${localURL}"]`));assert.ok(revoked.includes(localURL));await wait(()=>snapshot.document.nodes.some(n=>n.data.src===asset.url),'original reference saved'); - // 确认资源引用只保存一次,并保留上传期间的标题、位置和尺寸修改。 + // 确认最终快照只有一个该资源节点,并保留标题;位置/尺寸比较的是当前 DOM,不逐字段检查持久化坐标。 + // saves 可以含多次 PATCH,本断言不等于“资源引用只保存一次”。 const persisted=snapshot.document.nodes.find(n=>n.id===imageNodeID);assert.equal(persisted.data.title,'上传前改名');assert.equal(editable().style.transform,editedPosition);assert.equal(editable().style.width,editedWidth);assert.equal(editable().style.height,editedHeight); assert.equal(snapshot.document.nodes.filter(n=>n.data.src===asset.url).length,1);assert.equal(snapshot.document.nodes.find(n=>n.data.src===asset.url).data.assetId,assetID); console.log('PASS uploads: immediate local preview, unrelated editing, original-byte attachment and no blob URL persistence'); // 403 后预览和文件仍保留;手动重试沿用第一次幂等键,最终只落一个节点。 + // 这是旧上传交互的重试场景;重试由用户点击,不是 headless 未知执行结果的自动回执查询流程。 await reset('failed');await choose();await wait(()=>document.querySelector('[aria-label="图片上传状态"]')?.textContent.includes('上传未完成'),'visible upload failure'); assert.ok(document.querySelector('img[src^="blob:"]'));assert.equal(saves.length,0);const key=uploads[0].key;mode='delayed'; await event([...document.querySelectorAll('[aria-label="图片上传状态"] button')].find(n=>n.textContent==='重试'),'click');assert.equal(uploads.length,2);assert.equal(uploads[1].key,key,'Manual retry retains the original idempotency key'); diff --git a/tests/canvas-v2-scenario.mjs b/tests/canvas-v2-scenario.mjs index 6b84f17..f944836 100644 --- a/tests/canvas-v2-scenario.mjs +++ b/tests/canvas-v2-scenario.mjs @@ -79,7 +79,7 @@ export async function canvasV2Scenario({root,act,h,GraphCanvas,window,makeCanvas records.get(parent).document.edges.push({id:input.edge.id,source:'linked-frame',sourceHandle:'port:generated-port',target:'outside',targetHandle:'left'}); records.get(parent).version++;records.get(child).version++; connectionReceipt={transaction_id:'tx-connect',parent:{board_id:parent,...structuredClone(records.get(parent))},snapshots:[{board_id:parent,...structuredClone(records.get(parent))},{board_id:child,...structuredClone(records.get(child))}],undo_receipt:{expires_at:new Date(Date.now()+86400000).toISOString()}}; - return json({error:'upstream_unavailable'},502); // Simulate a lost response after the atomic commit. + return json({error:'upstream_unavailable'},502); // 内存记录已更新后丢失响应,客户端须以原 key/body 取回回执。 } // 创建的撤销检查当前父/子版本,以新父版本恢复原内容并返回归档子 ID;回执由内存生成。 if(suffix.startsWith('transactions/tx-create-')&&suffix.endsWith('/undo')&&method==='POST'){ @@ -122,7 +122,7 @@ export async function canvasV2Scenario({root,act,h,GraphCanvas,window,makeCanvas await event(document.querySelector('[aria-label="继续向下传播"]'),'click'); await event(document.querySelector('[aria-label="执行 AI"]'),'click'); assert.ok(job);assert.equal(document.querySelector('[aria-label="AI 模式"]').getAttribute('aria-pressed'),'true'); - // Move a child while the run executes. Its original owner receives the save. + // 生成期间移动子节点,后续检查实际 PATCH 发给 child;父回执不能把子节点另存成父成员。 await event(node(),'mousedown',{clientX:200,clientY:200,button:0,buttons:1});await event(window,'mousemove',{clientX:210,clientY:200,buttons:1});await event(window,'mousemove',{clientX:240,clientY:215,buttons:1});await event(window,'mouseup',{clientX:240,clientY:215,button:0}); // 有限轮询真实组件消费替身事件的结果;通过 act 推进 effect,而非直接替组件改 DOM。 const waitUntil=async(check,label)=>{for(let i=0;i<100;i++){if(check())return;await act(async()=>{await new Promise(r=>setTimeout(r,80));});}throw new Error('Timed out: '+label);}; @@ -156,6 +156,7 @@ export async function canvasV2Scenario({root,act,h,GraphCanvas,window,makeCanvas document.elementFromPoint=originalHitTest; await waitUntil(()=>records.get(parent).document.nodes.find(node=>node.id==='linked-frame').data.ports.some(port=>port.id==='generated-port'),'cross-canvas connection transaction'); // 等待不确定结果提示再点击重试;两次请求最终只产生一条额外边。 + // 本夹具只测试这一层父/子的共同上级连线事务,不据此证明任意深度 canvas 树的 LCA 计算。 await waitUntil(()=>[...document.querySelectorAll('button')].some(button=>button.textContent==='重试同步'),'uncertain transaction recovery'); await event([...document.querySelectorAll('button')].find(button=>button.textContent==='重试同步'),'click'); assert.equal(connectionRequests,2);assert.equal(document.querySelector('.react-flow__edge[data-id="'+connectionReceipt.parent.document.edges.at(-1).id+'"]')!==null,true);