mirror of https://github.com/langgenius/dify.git
test(workflow): add tests for edge interactions, curl panel parsing, and iteration log behavior
- Implement tests for handling edge deletion by ID and source handle changes in useEdgesInteractions. - Update curl-panel tests to use the curlParser module for parsing commands. - Add tests to ensure iteration log correctly handles empty structured lists and counts failed iterations based on execution data.
This commit is contained in:
parent
98b2a36219
commit
d8704d7124
|
|
@ -291,6 +291,17 @@ describe('useEdgesInteractions', () => {
|
|||
expect(mockSaveStateToHistory).toHaveBeenCalledWith('EdgeDelete')
|
||||
})
|
||||
|
||||
it('handleEdgeDeleteById should ignore unknown edge ids', () => {
|
||||
const { result } = renderEdgesInteractions()
|
||||
|
||||
act(() => {
|
||||
result.current.handleEdgeDeleteById('missing-edge')
|
||||
})
|
||||
|
||||
expect(result.current.edges).toHaveLength(2)
|
||||
expect(mockSaveStateToHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handleEdgeDeleteByDeleteBranch should remove edges for the given branch', async () => {
|
||||
const { result, store } = renderEdgesInteractions({
|
||||
initialStoreState: {
|
||||
|
|
@ -335,6 +346,46 @@ describe('useEdgesInteractions', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('handleEdgeSourceHandleChange should clear edgeMenu and save history for affected edges', async () => {
|
||||
const { result, store } = renderEdgesInteractions({
|
||||
edges: [
|
||||
createEdge({
|
||||
id: 'n1-old-handle-n2-target',
|
||||
source: 'n1',
|
||||
target: 'n2',
|
||||
sourceHandle: 'old-handle',
|
||||
targetHandle: 'target',
|
||||
data: {},
|
||||
}),
|
||||
],
|
||||
initialStoreState: {
|
||||
edgeMenu: { clientX: 120, clientY: 60, edgeId: 'n1-old-handle-n2-target' },
|
||||
},
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleEdgeSourceHandleChange('n1', 'old-handle', 'new-handle')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.edges[0]?.sourceHandle).toBe('new-handle')
|
||||
})
|
||||
|
||||
expect(store.getState().edgeMenu).toBeUndefined()
|
||||
expect(mockSaveStateToHistory).toHaveBeenCalledWith('EdgeSourceHandleChange')
|
||||
})
|
||||
|
||||
it('handleEdgeSourceHandleChange should do nothing when no edges use the old handle', () => {
|
||||
const { result } = renderEdgesInteractions()
|
||||
|
||||
act(() => {
|
||||
result.current.handleEdgeSourceHandleChange('n1', 'missing-handle', 'new-handle')
|
||||
})
|
||||
|
||||
expect(result.current.edges.map(edge => edge.id)).toEqual(['e1', 'e2'])
|
||||
expect(mockSaveStateToHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('read-only mode', () => {
|
||||
beforeEach(() => {
|
||||
mockReadOnly = true
|
||||
|
|
@ -412,5 +463,27 @@ describe('useEdgesInteractions', () => {
|
|||
|
||||
expect(result.current.edges).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('handleEdgeSourceHandleChange should do nothing', () => {
|
||||
const { result } = renderEdgesInteractions({
|
||||
edges: [
|
||||
createEdge({
|
||||
id: 'n1-old-handle-n2-target',
|
||||
source: 'n1',
|
||||
target: 'n2',
|
||||
sourceHandle: 'old-handle',
|
||||
targetHandle: 'target',
|
||||
data: {},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleEdgeSourceHandleChange('n1', 'old-handle', 'new-handle')
|
||||
})
|
||||
|
||||
expect(result.current.edges[0]?.sourceHandle).toBe('old-handle')
|
||||
expect(mockSaveStateToHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
|
|||
import userEvent from '@testing-library/user-event'
|
||||
import { BodyPayloadValueType, BodyType } from '../../types'
|
||||
import CurlPanel from '../curl-panel'
|
||||
import { parseCurl } from '../curl-parser'
|
||||
import * as curlParser from '../curl-parser'
|
||||
|
||||
const {
|
||||
mockHandleNodeSelect,
|
||||
|
|
@ -31,7 +31,7 @@ describe('curl-panel', () => {
|
|||
|
||||
describe('parseCurl', () => {
|
||||
it('should parse method, headers, json body, and query params from a valid curl command', () => {
|
||||
const { node, error } = parseCurl('curl -X POST -H \"Authorization: Bearer token\" --json \"{\"name\":\"openai\"}\" https://example.com/users?page=1&size=2')
|
||||
const { node, error } = curlParser.parseCurl('curl -X POST -H \"Authorization: Bearer token\" --json \"{\"name\":\"openai\"}\" https://example.com/users?page=1&size=2')
|
||||
|
||||
expect(error).toBeNull()
|
||||
expect(node).toMatchObject({
|
||||
|
|
@ -43,11 +43,11 @@ describe('curl-panel', () => {
|
|||
})
|
||||
|
||||
it('should return an error for invalid curl input', () => {
|
||||
expect(parseCurl('fetch https://example.com').error).toContain('Invalid cURL command')
|
||||
expect(curlParser.parseCurl('fetch https://example.com').error).toContain('Invalid cURL command')
|
||||
})
|
||||
|
||||
it('should parse form data and attach typed content headers', () => {
|
||||
const { node, error } = parseCurl('curl --request POST --form "file=@report.txt;type=text/plain" --form "name=openai" https://example.com/upload')
|
||||
const { node, error } = curlParser.parseCurl('curl --request POST --form "file=@report.txt;type=text/plain" --form "name=openai" https://example.com/upload')
|
||||
|
||||
expect(error).toBeNull()
|
||||
expect(node).toMatchObject({
|
||||
|
|
@ -62,7 +62,7 @@ describe('curl-panel', () => {
|
|||
})
|
||||
|
||||
it('should parse raw payloads and preserve equals signs in the body value', () => {
|
||||
const { node, error } = parseCurl('curl --data-binary "token=abc=123" https://example.com/raw')
|
||||
const { node, error } = curlParser.parseCurl('curl --data-binary "token=abc=123" https://example.com/raw')
|
||||
|
||||
expect(error).toBeNull()
|
||||
expect(node?.body).toEqual({
|
||||
|
|
@ -83,7 +83,7 @@ describe('curl-panel', () => {
|
|||
['curl --form "=broken" https://example.com/upload', 'Invalid form data format.'],
|
||||
['curl -H "Accept: application/json"', 'Missing URL or url not start with http.'],
|
||||
])('should return a descriptive error for %s', (command, expectedError) => {
|
||||
expect(parseCurl(command)).toEqual({
|
||||
expect(curlParser.parseCurl(command)).toEqual({
|
||||
node: null,
|
||||
error: expectedError,
|
||||
})
|
||||
|
|
@ -135,5 +135,31 @@ describe('curl-panel', () => {
|
|||
type: 'error',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should keep the panel open when parsing returns no node and no error', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onHide = vi.fn()
|
||||
const handleCurlImport = vi.fn()
|
||||
vi.spyOn(curlParser, 'parseCurl').mockReturnValueOnce({
|
||||
node: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(
|
||||
<CurlPanel
|
||||
nodeId="node-1"
|
||||
isShow
|
||||
onHide={onHide}
|
||||
handleCurlImport={handleCurlImport}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(onHide).not.toHaveBeenCalled()
|
||||
expect(handleCurlImport).not.toHaveBeenCalled()
|
||||
expect(mockHandleNodeSelect).not.toHaveBeenCalled()
|
||||
expect(mockNotify).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -107,6 +107,43 @@ describe('useNodeIterationInteractions', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('should rerender the parent iteration node when a child size changes', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createIterationNode({
|
||||
id: 'iteration-node',
|
||||
width: 120,
|
||||
height: 80,
|
||||
data: { width: 120, height: 80 },
|
||||
}),
|
||||
createNode({
|
||||
id: 'child-node',
|
||||
parentId: 'iteration-node',
|
||||
position: { x: 100, y: 90 },
|
||||
width: 60,
|
||||
height: 40,
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeIterationInteractions())
|
||||
result.current.handleNodeIterationChildSizeChange('child-node')
|
||||
|
||||
expect(mockSetNodes).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should skip iteration rerender when the resized node has no parent', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createNode({
|
||||
id: 'standalone-node',
|
||||
data: { type: BlockEnum.Code, title: 'Standalone', desc: '' },
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeIterationInteractions())
|
||||
result.current.handleNodeIterationChildSizeChange('standalone-node')
|
||||
|
||||
expect(mockSetNodes).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should copy iteration children and remap ids', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createIterationNode({ id: 'iteration-node' }),
|
||||
|
|
|
|||
|
|
@ -100,6 +100,43 @@ describe('useNodeLoopInteractions', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('should rerender the parent loop node when a child size changes', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createLoopNode({
|
||||
id: 'loop-node',
|
||||
width: 120,
|
||||
height: 80,
|
||||
data: { width: 120, height: 80 },
|
||||
}),
|
||||
createNode({
|
||||
id: 'child-node',
|
||||
parentId: 'loop-node',
|
||||
position: { x: 100, y: 90 },
|
||||
width: 60,
|
||||
height: 40,
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
result.current.handleNodeLoopChildSizeChange('child-node')
|
||||
|
||||
expect(mockSetNodes).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should skip loop rerender when the resized node has no parent', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createNode({
|
||||
id: 'standalone-node',
|
||||
data: { type: BlockEnum.Code, title: 'Standalone', desc: '' },
|
||||
}),
|
||||
])
|
||||
|
||||
const { result } = renderHook(() => useNodeLoopInteractions())
|
||||
result.current.handleNodeLoopChildSizeChange('standalone-node')
|
||||
|
||||
expect(mockSetNodes).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should copy loop children and remap ids', () => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
createLoopNode({ id: 'loop-node' }),
|
||||
|
|
|
|||
|
|
@ -129,5 +129,61 @@ describe('IterationLogTrigger', () => {
|
|||
|
||||
expect(onShowIterationResultList).toHaveBeenCalledWith(detailList, {})
|
||||
})
|
||||
|
||||
it('should return an empty structured list when duration map exists without executions', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onShowIterationResultList = vi.fn()
|
||||
const iterationDurationMap: IterationDurationMap = { orphaned: 1.5 }
|
||||
|
||||
render(
|
||||
<IterationLogTrigger
|
||||
nodeInfo={createNodeTracing({
|
||||
execution_metadata: createExecutionMetadata({
|
||||
iteration_duration_map: iterationDurationMap,
|
||||
}),
|
||||
})}
|
||||
onShowIterationResultList={onShowIterationResultList}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(onShowIterationResultList).toHaveBeenCalledWith([], iterationDurationMap)
|
||||
})
|
||||
|
||||
it('should count failed iterations from allExecutions and ignore unmatched duration map keys', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onShowIterationResultList = vi.fn()
|
||||
const iterationDurationMap: IterationDurationMap = { orphaned: 0.6, 1: 1.1 }
|
||||
const allExecutions = [
|
||||
createNodeTracing({
|
||||
id: 'failed-serial-step',
|
||||
status: NodeRunningStatus.Failed,
|
||||
execution_metadata: createExecutionMetadata({
|
||||
iteration_id: 'iteration-node',
|
||||
iteration_index: 1,
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
render(
|
||||
<IterationLogTrigger
|
||||
nodeInfo={createNodeTracing({
|
||||
details: [[createNodeTracing({ id: 'detail-success' })]],
|
||||
execution_metadata: createExecutionMetadata({
|
||||
iteration_duration_map: iterationDurationMap,
|
||||
}),
|
||||
})}
|
||||
allExecutions={allExecutions}
|
||||
onShowIterationResultList={onShowIterationResultList}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /workflow\.nodes\.iteration\.error/i })).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(onShowIterationResultList).toHaveBeenCalledWith([[allExecutions[0]]], iterationDurationMap)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue