Files
BlackVortexAI-bv_nodepack/tests/react_node_widget_host.test.mjs

154 lines
10 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {configureReactNodeWidgetHost,installReactNodeWidgetHost,intrinsicNodeWidgetContentHeight,refreshReactNodeWidget} from "../ui/src/regional/reactNodeWidgetHost.tsx";
test("intrinsic DOM widget measurement ignores height stretched by the node",()=>{
const content={
children:[{offsetTop:0,scrollHeight:318,offsetHeight:318,getBoundingClientRect:()=>({height:318})}],
scrollHeight:820,
getBoundingClientRect:()=>({height:820}),
};
assert.equal(intrinsicNodeWidgetContentHeight(content),318);
});
test("central React node widget host mounts once, rerenders and owns cleanup",()=>{
const events=[];
const host={className:"",dataset:{},remove(){events.push("remove")}};
const content={className:"",dataset:{}};
configureReactNodeWidgetHost({
createHost:()=>host,
createContentHost:()=>content,
createRoot:element=>{assert.equal(element,content);return{render:value=>events.push(["render",value]),unmount:()=>events.push("unmount")}},
schedule:action=>action(),
applyPresentation:(_node,nodeType)=>events.push(["presentation",nodeType]),
});
function NodeType(){}
NodeType.prototype.onNodeCreated=function(){events.push("created")};
NodeType.prototype.onConfigure=function(){events.push("configured")};
NodeType.prototype.onRemoved=function(){events.push("removed")};
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",{id:"registry",name:"registry_widget",minHeight:220,render:node=>`view-${node.value}`});
const node=new NodeType();node.value=1;node.addDOMWidget=(name,type,element,options)=>{events.push(["add",name,type,element,options.getMinHeight()]);return{serialize:true}};
node.onNodeCreated();node.value=2;node.onConfigure();node.onRemoved();
assert.equal(events.filter(event=>Array.isArray(event)&&event[0]==="add").length,1);
assert.deepEqual(events.filter(event=>Array.isArray(event)&&event[0]==="render").map(event=>event[1]),["view-1","view-2"]);
assert.deepEqual(events.filter(event=>Array.isArray(event)&&event[0]==="presentation").map(event=>event[1]),["BV LoRA Registry","BV LoRA Registry"]);
assert.deepEqual(events.slice(-3),["unmount","remove","removed"]);
});
test("central React node widget host tracks content height and disconnects measurement",()=>{
const events=[];let resize,disconnects=0,options;
const host={className:"",dataset:{},remove(){events.push("remove")}};
const content={className:"",dataset:{}};
configureReactNodeWidgetHost({
createHost:()=>host,
createContentHost:()=>content,
createRoot:()=>({render:()=>{},unmount:()=>events.push("unmount")}),
schedule:action=>action(),
applyPresentation:()=>events.push("presentation"),
observeHost:(element,onHeight)=>{assert.equal(element,content);resize=onHeight;return()=>{disconnects++}},
});
function NodeType(){}
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",{id:"registry-size",name:"registry_widget",minHeight:72,render:()=>"view"});
const node=new NodeType();node.addDOMWidget=(_name,_type,_host,next)=>{options=next;return{serialize:true}};
node.onNodeCreated();
assert.equal(typeof resize,"function");assert.equal(options.getMinHeight(),72);
resize(344);assert.equal(options.getMinHeight(),72);assert.equal(events.filter(item=>item==="presentation").length,2);
resize(344);assert.equal(events.filter(item=>item==="presentation").length,2);
resize(40);assert.equal(options.getMinHeight(),72);
node.onRemoved();assert.equal(disconnects,1);
});
test("central React node widget host measures intrinsic content and coalesces resize reconciliation",()=>{
const outer={className:"",dataset:{},remove(){}},content={className:"",dataset:{}},queue=[];let resize,disconnects=0,options,presentations=0;
configureReactNodeWidgetHost({
createHost:()=>outer,
createContentHost:host=>{assert.equal(host,outer);return content},
createRoot:element=>{assert.equal(element,content);return{render:()=>{},unmount:()=>{}}},
schedule:action=>queue.push(action),
applyPresentation:()=>{presentations++},
observeHost:(element,onHeight)=>{assert.equal(element,content);resize=onHeight;return()=>{disconnects++}},
});
function NodeType(){}
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",{id:"registry-intrinsic",name:"registry_widget",minHeight:72,render:()=>"view"});
const node=new NodeType();node.addDOMWidget=(_name,_type,element,next)=>{assert.equal(element,outer);options=next;return{serialize:true}};
node.onNodeCreated();assert.equal(queue.length,1);queue.shift()();assert.equal(presentations,1);
resize(180);resize(240);assert.equal(queue.length,1);assert.equal(options.getMinHeight(),72);queue.shift()();assert.equal(presentations,2);
resize(120);assert.equal(queue.length,1);queue.shift()();assert.equal(options.getMinHeight(),72);assert.equal(presentations,3);
node.onRemoved();resize(300);assert.equal(queue.length,0);assert.equal(disconnects,1);
});
test("central React node widget host cancels pending mounts after node removal",()=>{
const queue=[];let adds=0,roots=0,observers=0;
const outer=()=>({className:"",dataset:{},remove(){}}),content=()=>({className:"",dataset:{}});
configureReactNodeWidgetHost({
createHost:outer,createContentHost:()=>content(),
createRoot:()=>{roots++;return{render:()=>{},unmount:()=>{}}},
schedule:action=>queue.push(action),applyPresentation:()=>{},
observeHost:()=>{observers++;return()=>{}},
});
function NodeType(){}
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",{id:"registry-cancel",name:"registry_widget",minHeight:72,render:()=>"view"});
const makeNode=()=>{const node=new NodeType();node.addDOMWidget=()=>{adds++;return{serialize:true}};return node};
const created=makeNode();created.onNodeCreated();created.onRemoved();while(queue.length)queue.shift()();
const configured=makeNode();configured.onConfigure();configured.onRemoved();while(queue.length)queue.shift()();
assert.equal(adds,0);assert.equal(roots,0);assert.equal(observers,0);
assert.equal(created.__bvReactNodeWidgets,undefined);assert.equal(configured.__bvReactNodeWidgets,undefined);
});
test("central React node widget host rejects direct refresh after node removal",()=>{
let adds=0,roots=0,observers=0;
configureReactNodeWidgetHost({
createHost:()=>({className:"",dataset:{},remove(){}}),createContentHost:()=>({className:"",dataset:{}}),
createRoot:()=>{roots++;return{render:()=>{},unmount:()=>{}}},schedule:action=>action(),applyPresentation:()=>{},
observeHost:()=>{observers++;return()=>{}},
});
function NodeType(){}
const spec={id:"registry-refresh",name:"registry_widget",minHeight:72,render:()=>"view"};
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",spec);
const node=new NodeType();node.addDOMWidget=()=>{adds++;return{serialize:true}};
node.onNodeCreated();assert.deepEqual([adds,roots,observers],[1,1,1]);node.onRemoved();
assert.equal(refreshReactNodeWidget(node,"BV LoRA Registry",spec),null);
assert.deepEqual([adds,roots,observers],[1,1,1]);assert.equal(node.__bvReactNodeWidgets,undefined);
});
test("central React node widget host caps intrinsic height against both pixels and viewport",()=>{
const events=[];let resize,options,viewportHeight=900;
const host={className:"",dataset:{},style:{},remove(){}},content={className:"",dataset:{},style:{}};
configureReactNodeWidgetHost({
createHost:()=>host,createContentHost:()=>content,
createRoot:()=>({render:()=>{},unmount:()=>{}}),schedule:action=>action(),applyPresentation:()=>events.push("presentation"),
viewportHeight:()=>viewportHeight,
observeHost:(_element,onHeight)=>{resize=onHeight;return()=>{}},
});
function NodeType(){}
const spec={id:"registry-cap",name:"registry_widget",minHeight:72,maxHeight:420,overflow:"auto",nativeActions:[{id:"open",name:"open_lora_registry",label:"Open LoRA Registry",invoke:node=>events.push(["open",node.id])}],render:()=>"view"};
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",spec);
const node=new NodeType();node.id=9;node.widgets=[];
node.addWidget=(type,name,_value,callback,next)=>{const widget={type,name,callback,serialize:true,spec:{existing:"kept"},...next};node.widgets.push(widget);events.push(["action",type,name]);return widget};
node.addDOMWidget=(_name,_type,_host,next)=>{options=next;events.push("dom");return{serialize:true}};
node.onNodeCreated();node.onConfigure();
assert.equal(events.filter(item=>Array.isArray(item)&&item[0]==="action").length,1);
assert.deepEqual(node.widgets[0].spec,{existing:"kept",socketless:true});
assert.equal(node.widgets[0].serialize,false);node.widgets[0].callback();assert.deepEqual(events.at(-1),["open",9]);
resize(900);assert.equal(options.getMinHeight(),72);assert.equal(options.getMaxHeight(),420);
viewportHeight=500;assert.equal(options.getMinHeight(),72);assert.equal(options.getMaxHeight(),300);
resize(900);assert.equal(options.getMinHeight(),72);
viewportHeight=900;
resize(180);assert.equal(options.getMinHeight(),72);
assert.match(content.className,/bv-react-node-widget-scroll/);
assert.equal(options.margin,10);assert.equal(options.getHeight,undefined);assert.equal(content.style.maxHeight,"min(420px, 60vh, 100%)");assert.equal(content.style.overflowY,"auto");
});
test("opt-in content growth caps intrinsic height and yields to explicit user height",()=>{
let resize,options;const content={className:"",style:{}};
configureReactNodeWidgetHost({createHost:()=>({className:"",dataset:{},remove(){}}),createContentHost:()=>content,createRoot:()=>({render(){},unmount(){}}),schedule:action=>action(),applyPresentation:()=>{},viewportHeight:()=>1000,observeHost:(_host,measure)=>{resize=measure;return()=>{}}});
function NodeType(){}
installReactNodeWidgetHost(NodeType,"BV LoRA Registry",{id:"grow",name:"grow",minHeight:72,maxHeight:340,growWithContent:true,render:()=>null});
const node=new NodeType();node.addDOMWidget=(_name,_type,_host,value)=>{options=value;return{}};node.onNodeCreated();
resize(180);assert.equal(options.getMinHeight(),180);
resize(700);assert.equal(options.getMinHeight(),340);
node.properties={bvPresentationSizeVersion:1,bvPresentationUserHeight:300};
resize(800);assert.equal(options.getMinHeight(),72);assert.equal(options.getMaxHeight(),Infinity);assert.equal(content.style.maxHeight,"100%");
node.onRemoved();
});