docs(tests): explain React fixture timing and render counters

This commit is contained in:
2026-11-12 18:07:32 +08:00
parent 63aa3cf006
commit a582052a1e
4 changed files with 6 additions and 1 deletions
+1
View File
@@ -30,6 +30,7 @@ export async function run(){
// 将编译哈希 Worker 结果与浏览器原生 SHA-256 对照,不使用预设摘要跳过实际字节读取。
const digest=await digests.digest(bytes),expected=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await bytes.arrayBuffer()))].map(value=>value.toString(16).padStart(2,'0')).join('');assert(digest===expected,'compiled digest Worker SHA256');
// Worker 必须请求发布的 .js 文件,任务结束后活跃数为零;这验证 URL 布局和正常结束时的释放。
// active 来自构造/terminate 包装计数,检查正常任务是否调用终止清理,不是浏览器进程或 OS 线程盘点。
assert(workerUrls.some(url=>url.endsWith('/image.worker.js'))&&workerUrls.some(url=>url.endsWith('/digest.worker.js')),'Worker URLs did not resolve emitted JS');assert(active===0,'compiled Workers leaked');
results.push('compiled image and digest module Workers load emitted JS, decode a real PNG, match native SHA256 and terminate');
// 再验证 16 MiB 数据摘要及释放;耗时仅记为本机观察值,没有性能 SLA 断言。
+2
View File
@@ -55,6 +55,7 @@ async function run(){
// 两个 slot 可同时借用同一资源,分别保存 hook 结果以检查共享 URL 及引用计数释放。
const Asset=memo(function Asset({id,slot}:{id:string;slot:'a'|'b'}){const value=useAsset(id);if(slot==='a')assetA=value;else assetB=value;return null;});
// 每次以 StrictMode 和 CanvasProvider 同步替换子树;owner 可替换成另一个 engine,随后等待 effects。
// flushSync 只同步本轮 React 更新;异步 hook/资源工作仍通过后面的 tick/until 观察,不能据此推定引擎已 settle。
const render=async(children:any,owner=engine)=>{flushSync(()=>root.render(<StrictMode><CanvasProvider engine={owner} viewId="v">{children}</CanvasProvider></StrictMode>));await tick();};
try{
// 先等 synced 再记录动态基线;StrictMode 可能重复渲染,因此断言增量而非写死初始次数。
@@ -70,6 +71,7 @@ async function run(){
// 第二次手势提交只新增一个历史单元,租约释放后旧 commit 不再可写。
assert(gesture.begin().enabled,'second gesture did not acquire');gesture.update({x:55});assert(gesture.commit().status==='applied','gesture did not commit');await engine.settle();assert(engine.getSnapshot().history.undo.length===history+1,'gesture did not create exactly one history entry');assert(gesture.commit().status==='rejected','released gesture lease remained writable');reports.push('gesture preview/cancel/commit owns a token and one history unit; modes stay scoped to their view');
// 切换 hook 目标后立即在 layout effect 开新租约;旧控制器的 cancel/commit 和 cleanup 都不能碰新目标。
// 旧 facade 与新 layout effect 同时存在于替换窗口;检查 token 不变比只检查最终目标更能发现旧 cleanup 误取消。
const previousGesture=gesture,beforeOwnerSwap=engine.getSnapshot();await render(<><Title/><Element/><ElementTitle/><Actions/><Mode/><Gesture targetId="b" beginOnLayout/><Operation id={renamed.operationIds[0]}/><Status/></>);const replacementToken=engine.sessions.getSnapshot().gesture?.token;assert(!!replacementToken&&engine.sessions.getSnapshot().gesture?.targets[0].id==='b','old target cleanup canceled the new layout gesture');assert(previousGesture.commit().status==='rejected','old controls committed the replacement gesture');previousGesture.cancel();assert(engine.sessions.getSnapshot().gesture?.token===replacementToken,'old controls canceled the replacement gesture');assert(engine.getSnapshot()===beforeOwnerSwap,'owner switch or obsolete callback published document/history work');gesture.cancel();await render(<><Title/><Element/><ElementTitle/><Actions/><Mode/><Gesture/><Operation id={renamed.operationIds[0]}/><Status/></>);reports.push('gesture controller replacement owns an independent lease; old cleanup, cancel and commit cannot touch a new target gesture');
// 先检查只有 durability 完成也能清除 dirty,再注入 quota 验证 blocked/dirty,重试成功才恢复 synced。
+2 -1
View File
@@ -9,7 +9,8 @@ import { defineComponent, editable, movable } from '@/lib/canvas-core/index';
import type {ReactFlowProjectionNode as ProjectionNode,ReactFlowProjectionEdge as ProjectionEdge} from '@/lib/canvas-bindings/react-flow';
import type { CanvasRuntimeExtension } from '@/lib/canvas-bindings/react-flow';
import { useCanvasBinding,useElement, useCanvasRendererCommands } from '@/lib/canvas-bindings/react-flow';
// 按元素 ID 计数真实 renderer 调用,供外层检查无关元素是否重渲染,不以 DOM 数量代替渲染次数。
// 按元素 ID 计数 renderer 函数调用,供外层检查无关元素是否重渲染,不以 DOM 数量代替渲染次数。
// 计数发生在 render 阶段,可能含 StrictMode/重试调用,不等于 DOM 提交次数;外层应比较受控操作前后增量。
function rendered(id:string){const host=globalThis as typeof globalThis&{__fixtureRenders?:Record<string,number>};const counts=host.__fixtureRenders??={};counts[id]=(counts[id]??0)+1;}
// 通过 useElement 取得当前原记录,捕获有 scoped target 与无绑定两种旧 editing facade,交给外层验证桥接隔离。
function Meter({data}:NodeProps<ProjectionNode>){
@@ -22,6 +22,7 @@ export function NativeSelectionFixture(){
const store=useStoreApi(),[enabled,setEnabled]=useState(false);
useEffect(()=>{
// select 先清除再添加原生选中节点,selected 从 nodeLookup 查询;日志独立保存已收到的事件。
// resetSelectedElements 和 addSelectedNodes 是两个原生调用,未封装原子业务事务;事件日志可记录其中的中间选择。
const control:Control={enable:setEnabled,select(ids){store.getState().resetSelectedElements();store.getState().addSelectedNodes(ids);},selected(){return [...store.getState().nodeLookup.values()].filter(node=>node.selected).map(node=>node.id);}};
window.__fixtureNativeSelection=control;window.__fixtureNativeSelectionEvents??=[];
// 只移除本 effect 注册的同一控制对象,旧 cleanup 不删除新挂载的控制接口;事件日志不在此清空。