import { describe, expect, it } from 'vitest'; import { type Box, boxesInBand, frameBoxes } from '../../src/editor/graph/framing'; const LIMITS = { min: 0.25, max: 2.5 }; /** Two nodes side by side, the right one twice as tall. */ const boxes: Box[] = [ { id: 'a', x: 0, y: 0, w: 100, h: 60 }, { id: 'b', x: 200, y: 0, w: 100, h: 120 }, ]; describe('rubber band', () => { it('catches a node the band merely touches, not only one it encloses', () => { // The band clips a's right edge and never reaches b. expect(boxesInBand(boxes, { x1: 90, y1: 10, x2: 150, y2: 20 })).toEqual(['a']); }); it('catches both when the band spans them', () => { expect(boxesInBand(boxes, { x1: -10, y1: -10, x2: 310, y2: 130 })).toEqual(['a', 'b']); }); it('reads a band dragged up and to the left the same way', () => { expect(boxesInBand(boxes, { x1: 310, y1: 130, x2: -10, y2: -10 })).toEqual(['a', 'b']); }); it('catches nothing in empty space', () => { expect(boxesInBand(boxes, { x1: 120, y1: 200, x2: 180, y2: 260 })).toEqual([]); }); }); describe('fit', () => { /** Where a graph point lands on screen under a framing. */ const toScreen = (f: { zoom: number; panX: number; panY: number }, x: number, y: number) => ({ x: x * f.zoom + f.panX, y: y * f.zoom + f.panY, }); it('leaves no node outside the view – the reported bug', () => { // Wider than the view at zoom 1, so framing has to zoom out rather than just centre. const view = { w: 200, h: 200 }; const f = frameBoxes(boxes, view, 24, LIMITS); expect(f.zoom).toBeLessThan(1); for (const b of boxes) { const topLeft = toScreen(f, b.x, b.y); const bottomRight = toScreen(f, b.x + b.w, b.y + b.h); expect(topLeft.x).toBeGreaterThanOrEqual(0); expect(topLeft.y).toBeGreaterThanOrEqual(0); expect(bottomRight.x).toBeLessThanOrEqual(view.w); expect(bottomRight.y).toBeLessThanOrEqual(view.h); } }); it('measures each node by its own height', () => { // b is twice as tall as a: pretending both were 60 high would frame too little. const tall = frameBoxes([{ id: 'b', x: 0, y: 0, w: 100, h: 400 }], { w: 200, h: 200 }, 24, LIMITS); const short = frameBoxes([{ id: 'a', x: 0, y: 0, w: 100, h: 60 }], { w: 200, h: 200 }, 24, LIMITS); expect(tall.zoom).toBeLessThan(short.zoom); }); it('centres what it frames', () => { const view = { w: 200, h: 200 }; const f = frameBoxes(boxes, view, 24, LIMITS); const centre = toScreen(f, 150, 60); expect(centre.x).toBeCloseTo(100); expect(centre.y).toBeCloseTo(100); }); it('does not blow a small graph up past its natural size', () => { const f = frameBoxes([{ id: 'a', x: 0, y: 0, w: 10, h: 10 }], { w: 800, h: 600 }, 24, LIMITS); expect(f.zoom).toBe(1); }); it('never zooms out past the limit, however far the nodes are apart', () => { const far: Box[] = [ { id: 'a', x: 0, y: 0, w: 100, h: 60 }, { id: 'b', x: 100000, y: 0, w: 100, h: 60 }, ]; expect(frameBoxes(far, { w: 200, h: 200 }, 24, LIMITS).zoom).toBe(LIMITS.min); }); it('centres the view on an empty graph instead of returning nonsense', () => { const f = frameBoxes([], { w: 200, h: 100 }, 24, LIMITS); expect(f).toEqual({ zoom: 1, panX: 100, panY: 50 }); }); });