import type { ReactNode } from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import { Position } from 'reactflow'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import CustomEdge from '../custom-edge'
import { BlockEnum, NodeRunningStatus } from '../types'
const mockUseAvailableBlocks = vi.hoisted(() => vi.fn())
const mockUseNodesInteractions = vi.hoisted(() => vi.fn())
const mockBlockSelector = vi.hoisted(() => vi.fn())
const mockGradientRender = vi.hoisted(() => vi.fn())
vi.mock('reactflow', () => ({
BaseEdge: (props: {
id: string
path: string
style: {
stroke: string
strokeWidth: number
opacity: number
strokeDasharray?: string
}
}) => (
),
EdgeLabelRenderer: ({ children }: { children?: ReactNode }) => {children}
,
getBezierPath: () => ['M 0 0', 24, 48],
Position: {
Right: 'right',
Left: 'left',
},
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useAvailableBlocks: (...args: unknown[]) => mockUseAvailableBlocks(...args),
useNodesInteractions: () => mockUseNodesInteractions(),
}))
vi.mock('@/app/components/workflow/block-selector', () => ({
__esModule: true,
default: (props: {
open: boolean
onOpenChange: (open: boolean) => void
onSelect: (nodeType: string, pluginDefaultValue?: Record) => void
availableBlocksTypes: string[]
triggerClassName?: () => string
}) => {
mockBlockSelector(props)
return (
)
},
}))
vi.mock('@/app/components/workflow/custom-edge-linear-gradient-render', () => ({
__esModule: true,
default: (props: {
id: string
startColor: string
stopColor: string
}) => {
mockGradientRender(props)
return {props.id}
},
}))
describe('CustomEdge', () => {
const mockHandleNodeAdd = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockUseNodesInteractions.mockReturnValue({
handleNodeAdd: mockHandleNodeAdd,
})
mockUseAvailableBlocks.mockImplementation((nodeType: BlockEnum) => {
if (nodeType === BlockEnum.Code)
return { availablePrevBlocks: ['code', 'llm'] }
return { availableNextBlocks: ['llm', 'tool'] }
})
})
it('should render a gradient edge and insert a node between the source and target', () => {
render(
,
)
expect(screen.getByTestId('edge-gradient')).toHaveTextContent('edge-1')
expect(mockGradientRender).toHaveBeenCalledWith(expect.objectContaining({
id: 'edge-1',
startColor: 'var(--color-workflow-link-line-success-handle)',
stopColor: 'var(--color-workflow-link-line-error-handle)',
}))
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-stroke', 'url(#edge-1)')
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-opacity', '0.3')
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-dasharray', '8 8')
expect(screen.getByTestId('block-selector')).toHaveTextContent('llm')
expect(screen.getByTestId('block-selector').parentElement).toHaveStyle({
transform: 'translate(-50%, -50%) translate(24px, 48px)',
opacity: '0.7',
})
fireEvent.click(screen.getByTestId('block-selector'))
expect(mockHandleNodeAdd).toHaveBeenCalledWith(
{
nodeType: 'llm',
pluginDefaultValue: { provider: 'openai' },
},
{
prevNodeId: 'source-node',
prevNodeSourceHandle: 'source',
nextNodeId: 'target-node',
nextNodeTargetHandle: 'target',
},
)
})
it('should prefer the running stroke color when the edge is selected', () => {
render(
,
)
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-stroke', 'var(--color-workflow-link-line-handle)')
})
it('should use the fail-branch running color while the connected node is hovering', () => {
render(
,
)
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-stroke', 'var(--color-workflow-link-line-failure-handle)')
})
it('should fall back to the default edge color when no highlight state is active', () => {
render(
,
)
expect(screen.getByTestId('base-edge')).toHaveAttribute('data-stroke', 'var(--color-workflow-link-line-normal)')
expect(screen.getByTestId('block-selector')).toHaveAttribute('data-trigger-class', 'hover:scale-150 transition-all')
})
})