Files
Endoleap/tests/headless/renderer-extension.tsx
T

30 lines
4.0 KiB
TypeScript

/**
* 浏览器渲染集成专用插件:模型定义与 ReactFlow renderer 一起通过 CanvasRuntimeExtension 注入。
* 页面全局计数和 editing facade 仅用于测试观察,数据编辑仍经过生产命令/事务入口。
*/
"use client";
import {useScopedNodeEditing} from '@/components/canvas/node-editing';
import { Handle, Position, BaseEdge, getBezierPath, type NodeProps, type EdgeProps } from '@xyflow/react';
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 数量代替渲染次数。
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>){
const commands=useCanvasRendererCommands(),lookup=useElement(data.__headlessRecord!),record=lookup.status==='present'?lookup.element:data.__headlessRecord!;rendered(record.id);const legacy=useScopedNodeEditing(record.id,data.__headlessTarget),unbound=useScopedNodeEditing(record.id),binding=useCanvasBinding();const host=globalThis as typeof globalThis&{__fixtureEngine?:typeof binding.engine;__fixtureLegacy?:Record<string,typeof legacy>;__fixtureUnbound?:Record<string,typeof legacy>};host.__fixtureEngine=binding.engine;(host.__fixtureLegacy??={})[record.id]=legacy;(host.__fixtureUnbound??={})[record.id]=unbound;
// Increase 按钮阻止点击冒泡,并在一个事务里修改 metric 与 raw,要求只产生一个历史单元;自定义 Handle ID 供连线端口测试。
return <div data-renderer-fixture="meter" style={{width:'100%',height:'100%',background:'white',border:'1px solid #ded7ce',padding:16}}>
<span>Metric {String(record.data.metric)}</span><button className="nodrag" aria-label={`Increase ${record.id}`} onClick={event=>{event.stopPropagation();commands.transaction(tx=>{tx.dispatch({type:'element.data',target:{id:record.id,incarnation:record.incarnation},patch:{metric:Number(record.data.metric)+1}});tx.dispatch({type:'element.data',target:{id:record.id,incarnation:record.incarnation},patch:{raw:{retained:true,edited:true}}});},{label:'Increase metric'});}}>Increase</button>
<Handle type="source" id="custom-out" position={Position.Right}/><Handle type="source" id="custom-in" position={Position.Left}/>
</div>;
}
// 自定义边记录渲染次数,使用 ReactFlow Bezier path 绘制;标题从原 connection data 读取 metric。
function MeterEdge(props:EdgeProps<ProjectionEdge>){rendered(props.id);const [path]=getBezierPath(props);return <g data-renderer-fixture="metric-edge"><title>Metric {String(props.data?.__headlessRecord?.data.metric)}</title><BaseEdge id={props.id} path={path}/></g>;}
// 两种模型分别校验 metric,节点安装编辑/移动能力;renderer 键精确匹配 componentId、dataVersion 与 structure。
export const fixtureExtension:CanvasRuntimeExtension={
components:[defineComponent({id:'fixture:meter',dataVersion:1,structure:'node',validate:data=>Number.isSafeInteger(data.metric),features:[editable(),movable()],copy:{references:[],omit:[]}}),defineComponent({id:'fixture:metric-edge',dataVersion:1,structure:'connection',validate:data=>typeof data.metric==='number',features:[],copy:{references:[],omit:[]}})],
renderers:[{componentId:'fixture:meter',dataVersion:1,structure:'node',rendererId:'fixture-meter',render:Meter},{componentId:'fixture:metric-edge',dataVersion:1,structure:'connection',rendererId:'fixture-metric-edge',render:MeterEdge}],
};