diff --git a/frontend/src/components/canvas/__tests__/BaseNode.test.tsx b/frontend/src/components/canvas/__tests__/BaseNode.test.tsx index 8a2371b..3c5cf6c 100644 --- a/frontend/src/components/canvas/__tests__/BaseNode.test.tsx +++ b/frontend/src/components/canvas/__tests__/BaseNode.test.tsx @@ -5,11 +5,14 @@ import { BaseNode } from '../nodes/BaseNode' import type { NodeData } from '@/types' import type { Node } from '@xyflow/react' +let mockZoom = 1 + vi.mock('@xyflow/react', () => ({ Handle: () => null, Position: { Top: 'top', Bottom: 'bottom' }, NodeResizer: () => null, useUpdateNodeInternals: () => vi.fn(), + useViewport: () => ({ zoom: mockZoom }), })) vi.mock('@/stores/themeStore', () => ({ @@ -85,6 +88,37 @@ function renderBaseNode(data: Partial) { ) } +describe('BaseNode — borderWidth zoom scaling', () => { + it('borderWidth is 1px at zoom=1', () => { + mockZoom = 1 + const { container } = renderBaseNode({}) + expect((container.firstChild as HTMLElement).style.borderWidth).toBe('1px') + }) + + it('borderWidth scales to 2px at zoom=0.5', () => { + mockZoom = 0.5 + const { container } = renderBaseNode({}) + expect((container.firstChild as HTMLElement).style.borderWidth).toBe('2px') + }) + + it('borderWidth is clamped to 1px at zoom=2', () => { + mockZoom = 2 + const { container } = renderBaseNode({}) + expect((container.firstChild as HTMLElement).style.borderWidth).toBe('1px') + }) + + it('boxShadow glow ring uses borderWidth when selected + online at zoom=0.5', () => { + mockZoom = 0.5 + const node = makeNode({ status: 'online' }) + const { container } = render( + + ) + expect((container.firstChild as HTMLElement).style.boxShadow).toContain('0 0 0 2px') + }) +}) + describe('BaseNode — properties rendering', () => { it('renders visible properties on the node', () => { renderBaseNode({ diff --git a/frontend/src/components/canvas/nodes/BaseNode.tsx b/frontend/src/components/canvas/nodes/BaseNode.tsx index 5be7dcf..e760c87 100644 --- a/frontend/src/components/canvas/nodes/BaseNode.tsx +++ b/frontend/src/components/canvas/nodes/BaseNode.tsx @@ -1,4 +1,4 @@ -import { createElement, useEffect } from 'react' +import { createElement, useEffect, useMemo } from 'react' import { Handle, Position, NodeResizer, useUpdateNodeInternals, useViewport, type NodeProps, type Node } from '@xyflow/react' import { Cpu, MemoryStick, HardDrive, type LucideIcon } from 'lucide-react' import type { NodeData } from '@/types' @@ -25,7 +25,7 @@ export function BaseNode({ id, data, selected, icon: typeIcon, width, height }: useEffect(() => { updateNodeInternals(id) }, [data.bottom_handles, id, updateNodeInternals]) const { zoom } = useViewport() - const borderWidth = Math.max(1, 1 / zoom) + const borderWidth = useMemo(() => Math.max(1, 1 / zoom), [zoom]) const activeTheme = useThemeStore((s) => s.activeTheme) const hideIp = useCanvasStore((s) => s.hideIp) @@ -49,11 +49,11 @@ export function BaseNode({ id, data, selected, icon: typeIcon, width, height }: borderColor: colors.border, borderWidth, boxShadow: isOnline && selected - ? `0 0 0 1px ${colors.border}, 0 0 10px ${colors.border}2e, 0 0 3px ${colors.border}1a` + ? `0 0 0 ${borderWidth}px ${colors.border}, 0 0 10px ${colors.border}2e, 0 0 3px ${colors.border}1a` : isOnline ? `0 0 10px ${colors.border}2e, 0 0 3px ${colors.border}1a` : selected - ? `0 0 0 1px ${colors.border}, 0 0 8px ${colors.border}44` + ? `0 0 0 ${borderWidth}px ${colors.border}, 0 0 8px ${colors.border}44` : 'none', opacity: data.status === 'offline' ? 0.55 : 1, minWidth: 140,