fix: memoize borderWidth and sync boxShadow glow ring with zoom
- Wrap borderWidth in useMemo to avoid re-computing on unrelated renders - Replace hardcoded 1px in boxShadow with borderWidth so glow ring matches border thickness at all zoom levels - Add useViewport mock + 4 zoom-scaling tests to BaseNode.test.tsx
This commit is contained in:
@@ -5,11 +5,14 @@ import { BaseNode } from '../nodes/BaseNode'
|
|||||||
import type { NodeData } from '@/types'
|
import type { NodeData } from '@/types'
|
||||||
import type { Node } from '@xyflow/react'
|
import type { Node } from '@xyflow/react'
|
||||||
|
|
||||||
|
let mockZoom = 1
|
||||||
|
|
||||||
vi.mock('@xyflow/react', () => ({
|
vi.mock('@xyflow/react', () => ({
|
||||||
Handle: () => null,
|
Handle: () => null,
|
||||||
Position: { Top: 'top', Bottom: 'bottom' },
|
Position: { Top: 'top', Bottom: 'bottom' },
|
||||||
NodeResizer: () => null,
|
NodeResizer: () => null,
|
||||||
useUpdateNodeInternals: () => vi.fn(),
|
useUpdateNodeInternals: () => vi.fn(),
|
||||||
|
useViewport: () => ({ zoom: mockZoom }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/stores/themeStore', () => ({
|
vi.mock('@/stores/themeStore', () => ({
|
||||||
@@ -85,6 +88,37 @@ function renderBaseNode(data: Partial<NodeData>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
<BaseNode id={node.id} data={node.data} selected={true} icon={Server}
|
||||||
|
type="server" dragging={false} zIndex={0} isConnectable={true}
|
||||||
|
positionAbsoluteX={0} positionAbsoluteY={0} />
|
||||||
|
)
|
||||||
|
expect((container.firstChild as HTMLElement).style.boxShadow).toContain('0 0 0 2px')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('BaseNode — properties rendering', () => {
|
describe('BaseNode — properties rendering', () => {
|
||||||
it('renders visible properties on the node', () => {
|
it('renders visible properties on the node', () => {
|
||||||
renderBaseNode({
|
renderBaseNode({
|
||||||
|
|||||||
@@ -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 { Handle, Position, NodeResizer, useUpdateNodeInternals, useViewport, type NodeProps, type Node } from '@xyflow/react'
|
||||||
import { Cpu, MemoryStick, HardDrive, type LucideIcon } from 'lucide-react'
|
import { Cpu, MemoryStick, HardDrive, type LucideIcon } from 'lucide-react'
|
||||||
import type { NodeData } from '@/types'
|
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])
|
useEffect(() => { updateNodeInternals(id) }, [data.bottom_handles, id, updateNodeInternals])
|
||||||
|
|
||||||
const { zoom } = useViewport()
|
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 activeTheme = useThemeStore((s) => s.activeTheme)
|
||||||
const hideIp = useCanvasStore((s) => s.hideIp)
|
const hideIp = useCanvasStore((s) => s.hideIp)
|
||||||
@@ -49,11 +49,11 @@ export function BaseNode({ id, data, selected, icon: typeIcon, width, height }:
|
|||||||
borderColor: colors.border,
|
borderColor: colors.border,
|
||||||
borderWidth,
|
borderWidth,
|
||||||
boxShadow: isOnline && selected
|
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
|
: isOnline
|
||||||
? `0 0 10px ${colors.border}2e, 0 0 3px ${colors.border}1a`
|
? `0 0 10px ${colors.border}2e, 0 0 3px ${colors.border}1a`
|
||||||
: selected
|
: 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',
|
: 'none',
|
||||||
opacity: data.status === 'offline' ? 0.55 : 1,
|
opacity: data.status === 'offline' ? 0.55 : 1,
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
|
|||||||
Reference in New Issue
Block a user