diff --git a/frontend/src/components/canvas/CanvasContainer.tsx b/frontend/src/components/canvas/CanvasContainer.tsx index 57741c7..3f8b91e 100644 --- a/frontend/src/components/canvas/CanvasContainer.tsx +++ b/frontend/src/components/canvas/CanvasContainer.tsx @@ -192,6 +192,7 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o colorMode={theme.colors.reactFlowColorMode} elevateNodesOnSelect={false} connectionMode={ConnectionMode.Loose} + connectionRadius={30} isValidConnection={isValidConnection} > { expect((container.firstChild as HTMLElement).style.borderWidth).toBe('1px') }) + // Regression: the node root must NOT clip its own bounds, otherwise the outer + // half of each connection handle (which sits centred on the edge) becomes + // non-interactive and the "magnet" snap area is halved. + it('root does not clip overflow (keeps handles grabbable)', () => { + const { container } = renderBaseNode({}) + expect((container.firstChild as HTMLElement).className).not.toContain('overflow-hidden') + }) + it('boxShadow glow ring uses borderWidth when selected + online at zoom=0.5', () => { mockZoom = 0.5 const node = makeNode({ status: 'online' }) diff --git a/frontend/src/components/canvas/nodes/BaseNode.tsx b/frontend/src/components/canvas/nodes/BaseNode.tsx index bce6d6f..fbfda1b 100644 --- a/frontend/src/components/canvas/nodes/BaseNode.tsx +++ b/frontend/src/components/canvas/nodes/BaseNode.tsx @@ -56,7 +56,7 @@ export function BaseNode({ id, data, selected, icon: typeIcon, width, height }: return (
) diff --git a/frontend/src/components/canvas/nodes/__tests__/SideHandles.test.tsx b/frontend/src/components/canvas/nodes/__tests__/SideHandles.test.tsx new file mode 100644 index 0000000..a0f5114 --- /dev/null +++ b/frontend/src/components/canvas/nodes/__tests__/SideHandles.test.tsx @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest' +import { render } from '@testing-library/react' +import { ReactFlowProvider } from '@xyflow/react' +import { SideHandles } from '../SideHandles' +import type { NodeData } from '@/types' + +function renderHandles(data: Partial = {}) { + const full: NodeData = { label: 'n', type: 'server', status: 'online', services: [], ...data } + return render( + + + + ) +} + +describe('SideHandles', () => { + it('renders a source + invisible target handle per slot', () => { + // default node: top=1, bottom=1, left=0, right=0 + const { container } = renderHandles({}) + expect(container.querySelectorAll('.react-flow__handle.source').length).toBe(2) + expect(container.querySelectorAll('.react-flow__handle.target').length).toBe(2) + }) + + it('target (magnet) handle hit area is large enough to snap onto (20px)', () => { + const { container } = renderHandles({}) + const target = container.querySelector('.react-flow__handle.target') as HTMLElement + expect(target.style.width).toBe('20px') + expect(target.style.height).toBe('20px') + expect(target.style.opacity).toBe('0') + }) + + it('renders configured per-side counts', () => { + const { container } = renderHandles({ top_handles: 2, left_handles: 3, right_handles: 1, bottom_handles: 1 }) + expect(container.querySelectorAll('.react-flow__handle.source').length).toBe(7) + }) +})