feat: add connection handles to zone nodes (closes #58)
- GroupRectNode now renders source+target handles on all four sides
(top, right, bottom, left) using IDs zone-{side} / zone-{side}-t
- Handles are hover-only: opacity 0 by default, fade in on mouse enter
- Handle color matches the zone border color (respects custom_colors)
- Zone↔zone and zone↔node connections both allowed; edge type picker
(EdgeModal) opens on connect so user chooses ethernet/wifi/vlan/etc.
- Add GroupRectNode.test.tsx: verifies 8 handles rendered (4 source + 4 target)
- Fix @xyflow/react mocks in LiveView and CanvasContainer tests to include Position
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { GroupRectNode } from '../nodes/GroupRectNode'
|
||||
import type { NodeData } from '@/types'
|
||||
import type { Node } from '@xyflow/react'
|
||||
|
||||
vi.mock('@xyflow/react', () => ({
|
||||
Handle: ({ id, type }: { id: string; type: string }) => <div data-testid={`handle-${id}`} data-type={type} />,
|
||||
Position: { Top: 'top', Right: 'right', Bottom: 'bottom', Left: 'left' },
|
||||
NodeResizer: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/canvasStore', () => ({
|
||||
useCanvasStore: (sel: (s: { setEditingGroupRectId: () => void }) => unknown) =>
|
||||
sel({ setEditingGroupRectId: vi.fn() }),
|
||||
}))
|
||||
|
||||
function makeNode(overrides: Partial<NodeData> = {}): Node<NodeData> {
|
||||
return {
|
||||
id: 'zone1',
|
||||
type: 'groupRect',
|
||||
position: { x: 0, y: 0 },
|
||||
data: { label: 'My Zone', type: 'groupRect', status: 'unknown', services: [], ...overrides },
|
||||
}
|
||||
}
|
||||
|
||||
function renderZone(overrides: Partial<NodeData> = {}) {
|
||||
const node = makeNode(overrides)
|
||||
return render(
|
||||
<GroupRectNode
|
||||
id={node.id}
|
||||
data={node.data}
|
||||
selected={false}
|
||||
type="groupRect"
|
||||
dragging={false}
|
||||
zIndex={0}
|
||||
isConnectable={true}
|
||||
positionAbsoluteX={0}
|
||||
positionAbsoluteY={0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('GroupRectNode — handles', () => {
|
||||
it('renders source handles on all four sides', () => {
|
||||
renderZone()
|
||||
expect(screen.getByTestId('handle-zone-top')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-zone-right')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-zone-bottom')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-zone-left')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders target handles on all four sides', () => {
|
||||
renderZone()
|
||||
expect(screen.getByTestId('handle-zone-top-t')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-zone-right-t')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-zone-bottom-t')).toBeDefined()
|
||||
expect(screen.getByTestId('handle-zone-left-t')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders 8 handles total (4 source + 4 target)', () => {
|
||||
renderZone()
|
||||
expect(screen.getAllByTestId(/^handle-zone-/).length).toBe(8)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GroupRectNode — label', () => {
|
||||
it('renders inside label by default', () => {
|
||||
renderZone({ label: 'DMZ' })
|
||||
expect(screen.getByText('DMZ')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders no label when label is empty', () => {
|
||||
renderZone({ label: '' })
|
||||
expect(screen.queryByText('DMZ')).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user