@@ -8,14 +8,32 @@ import { useBoundingBoxes } from './useBoundingBoxes'
88import type { BoundingBox } from '@/types/boundingBoxes'
99import { toNodeId } from '@/types/nodeId'
1010
11- const { appState } = vi . hoisted ( ( ) => ( {
12- appState : { node : null as unknown }
11+ const { appState, outputState } = vi . hoisted ( ( ) => ( {
12+ appState : { node : null as unknown } ,
13+ outputState : {
14+ outputs : undefined as unknown ,
15+ nodeOutputs : null as { value : Record < string , unknown > } | null
16+ }
1317} ) )
1418
1519vi . mock ( '@/scripts/app' , ( ) => ( {
1620 app : { canvas : { graph : { getNodeById : ( ) => appState . node } } }
1721} ) )
1822
23+ vi . mock ( '@/stores/nodeOutputStore' , async ( ) => {
24+ const { ref } = await import ( 'vue' )
25+ const nodeOutputs = ref < Record < string , unknown > > ( { } )
26+ outputState . nodeOutputs = nodeOutputs
27+ return {
28+ useNodeOutputStore : ( ) => ( {
29+ nodeOutputs,
30+ nodePreviewImages : ref ( { } ) ,
31+ getNodeImageUrls : ( ) => undefined ,
32+ getNodeOutputs : ( ) => outputState . outputs
33+ } )
34+ }
35+ } )
36+
1937const ctx = {
2038 measureText : ( s : string ) => ( { width : s . length * 7 } ) ,
2139 setTransform : ( ) => { } ,
@@ -27,6 +45,9 @@ const ctx = {
2745 save : ( ) => { } ,
2846 restore : ( ) => { } ,
2947 beginPath : ( ) => { } ,
48+ moveTo : ( ) => { } ,
49+ arc : ( ) => { } ,
50+ fill : ( ) => { } ,
3051 rect : ( ) => { } ,
3152 clip : ( ) => { } ,
3253 font : '' ,
@@ -128,9 +149,23 @@ const box = (over: Partial<BoundingBox> = {}): BoundingBox => ({
128149 ...over
129150} )
130151
152+ function makeConnectedNode ( ) {
153+ return {
154+ widgets : [
155+ { name : 'width' , value : 512 } ,
156+ { name : 'height' , value : 512 }
157+ ] ,
158+ findInputSlot : ( name : string ) => ( name === 'bboxes' ? 1 : - 1 ) ,
159+ getInputNode : ( ) => null ,
160+ isInputConnected : ( ) => true
161+ }
162+ }
163+
131164beforeEach ( ( ) => {
132165 setActivePinia ( createPinia ( ) )
133166 appState . node = makeNode ( )
167+ outputState . outputs = undefined
168+ if ( outputState . nodeOutputs ) outputState . nodeOutputs . value = { }
134169 vi . stubGlobal ( 'requestAnimationFrame' , ( cb : FrameRequestCallback ) => {
135170 void Promise . resolve ( ) . then ( ( ) => cb ( 0 ) )
136171 return 1
@@ -239,6 +274,77 @@ describe('useBoundingBoxes inline editor', () => {
239274 } )
240275} )
241276
277+ describe ( 'useBoundingBoxes incoming bboxes input' , ( ) => {
278+ it ( 'overrides the canvas when the bboxes input is connected' , ( ) => {
279+ appState . node = makeConnectedNode ( )
280+ outputState . outputs = {
281+ input_bboxes : [ box ( { x : 0 , y : 0 , width : 100 , height : 100 } ) ]
282+ }
283+ const c = setup ( [ ] )
284+ expect ( c . modelValue . value ) . toHaveLength ( 1 )
285+ expect ( c . modelValue . value [ 0 ] . width ) . toBe ( 100 )
286+ } )
287+
288+ it ( 'replaces existing drawn boxes with the incoming ones' , ( ) => {
289+ appState . node = makeConnectedNode ( )
290+ outputState . outputs = { input_bboxes : [ box ( { x : 0 , width : 100 } ) ] }
291+ const c = setup ( [ box ( { x : 200 , width : 300 } ) , box ( { x : 400 , width : 50 } ) ] )
292+ expect ( c . modelValue . value ) . toHaveLength ( 1 )
293+ expect ( c . modelValue . value [ 0 ] . width ) . toBe ( 100 )
294+ } )
295+
296+ it ( 'ignores incoming output when the input is not connected' , ( ) => {
297+ outputState . outputs = { input_bboxes : [ box ( { x : 0 , width : 100 } ) ] }
298+ const c = setup ( [ ] )
299+ expect ( c . modelValue . value ) . toHaveLength ( 0 )
300+ } )
301+
302+ it ( 'applies incoming boxes when outputs stream in after mount' , async ( ) => {
303+ appState . node = makeConnectedNode ( )
304+ const c = setup ( [ ] )
305+ expect ( c . modelValue . value ) . toHaveLength ( 0 )
306+
307+ outputState . outputs = { input_bboxes : [ box ( { x : 0 , width : 100 } ) ] }
308+ outputState . nodeOutputs ! . value = { updated : true }
309+ await flush ( )
310+
311+ expect ( c . modelValue . value ) . toHaveLength ( 1 )
312+ expect ( c . modelValue . value [ 0 ] . width ) . toBe ( 100 )
313+ } )
314+ } )
315+
316+ describe ( 'useBoundingBoxes grid snapping' , ( ) => {
317+ it ( 'snaps a drawn box to the grid when grid is enabled (default)' , async ( ) => {
318+ const c = setup ( )
319+ c . onPointerDown ( pe ( 10 , 10 ) )
320+ c . onCanvasPointerMove ( pe ( 60 , 60 ) )
321+ c . onDocPointerUp ( pe ( 60 , 60 ) )
322+ await flush ( )
323+ expect ( c . modelValue . value ) . toHaveLength ( 1 )
324+ expect ( c . modelValue . value [ 0 ] . x ) . toBe ( 64 )
325+ expect ( c . modelValue . value [ 0 ] . width ) . toBe ( 256 )
326+ } )
327+
328+ it ( 'does not snap when grid is disabled' , async ( ) => {
329+ const c = setup ( )
330+ c . grid . value = false
331+ c . onPointerDown ( pe ( 10 , 10 ) )
332+ c . onCanvasPointerMove ( pe ( 55 , 55 ) )
333+ c . onDocPointerUp ( pe ( 55 , 55 ) )
334+ await flush ( )
335+ expect ( c . modelValue . value [ 0 ] . width ) . toBe ( 230 )
336+ } )
337+
338+ it ( 'keeps the anchored edge fixed when resizing a single edge' , async ( ) => {
339+ const c = setup ( [ box ( { x : 51 , y : 51 , width : 256 , height : 256 } ) ] )
340+ c . onPointerDown ( pe ( 60 , 30 ) )
341+ c . onCanvasPointerMove ( pe ( 80 , 30 ) )
342+ c . onDocPointerUp ( pe ( 80 , 30 ) )
343+ await flush ( )
344+ expect ( c . modelValue . value [ 0 ] . x ) . toBe ( 51 )
345+ } )
346+ } )
347+
242348describe ( 'useBoundingBoxes hover cursor' , ( ) => {
243349 it ( 'switches to a pointer cursor over a tag' , async ( ) => {
244350 const c = setup ( [ box ( { x : 10 , y : 10 , width : 256 , height : 256 } ) ] )
0 commit comments