test(workflow): add comprehensive hooks unit tests and refactor test infrastructure (Part 3) (#32958)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-03-04 17:24:50 +08:00
committed by GitHub
parent 5a3348ec8d
commit 5385ec3023
37 changed files with 3615 additions and 729 deletions

View File

@ -1,9 +1,9 @@
import type { ConversationVariable } from '@/app/components/workflow/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import { createWorkflowStore } from '../workflow'
import { createTestWorkflowStore } from '../../__tests__/workflow-test-env'
function createStore() {
return createWorkflowStore({})
return createTestWorkflowStore()
}
describe('Chat Variable Slice', () => {

View File

@ -1,8 +1,8 @@
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { createWorkflowStore } from '../workflow'
import { createTestWorkflowStore } from '../../__tests__/workflow-test-env'
function createStore() {
return createWorkflowStore({})
return createTestWorkflowStore()
}
describe('Env Variable Slice', () => {

View File

@ -1,10 +1,10 @@
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { VarInInspectType } from '@/types/workflow'
import { createWorkflowStore } from '../workflow'
import { createTestWorkflowStore } from '../../__tests__/workflow-test-env'
function createStore() {
return createWorkflowStore({})
return createTestWorkflowStore()
}
function makeVar(overrides: Partial<VarInInspect> = {}): VarInInspect {

View File

@ -1,8 +1,8 @@
import type { VersionHistory } from '@/types/workflow'
import { createWorkflowStore } from '../workflow'
import { createTestWorkflowStore } from '../../__tests__/workflow-test-env'
function createStore() {
return createWorkflowStore({})
return createTestWorkflowStore()
}
describe('Version Slice', () => {

View File

@ -1,8 +1,8 @@
import type { Node } from '@/app/components/workflow/types'
import { createWorkflowStore } from '../workflow'
import { createTestWorkflowStore } from '../../__tests__/workflow-test-env'
function createStore() {
return createWorkflowStore({})
return createTestWorkflowStore()
}
describe('Workflow Draft Slice', () => {
@ -69,13 +69,20 @@ describe('Workflow Draft Slice', () => {
})
describe('debouncedSyncWorkflowDraft', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('should be a callable function', () => {
const store = createStore()
expect(typeof store.getState().debouncedSyncWorkflowDraft).toBe('function')
})
it('should debounce the sync call', () => {
vi.useFakeTimers()
const store = createStore()
const syncFn = vi.fn()
@ -84,12 +91,9 @@ describe('Workflow Draft Slice', () => {
vi.advanceTimersByTime(5000)
expect(syncFn).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
it('should flush pending sync via flushPendingSync', () => {
vi.useFakeTimers()
const store = createStore()
const syncFn = vi.fn()
@ -98,8 +102,6 @@ describe('Workflow Draft Slice', () => {
store.getState().flushPendingSync()
expect(syncFn).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
})
})

View File

@ -1,18 +1,29 @@
import type { Shape, SliceFromInjection } from '../workflow'
import type { HelpLineHorizontalPosition, HelpLineVerticalPosition } from '@/app/components/workflow/help-line/types'
import type { WorkflowRunningData } from '@/app/components/workflow/types'
import type { FileUploadConfigResponse } from '@/models/common'
import type { VersionHistory } from '@/types/workflow'
import { renderHook } from '@testing-library/react'
import * as React from 'react'
import { BlockEnum } from '@/app/components/workflow/types'
import { WorkflowContext } from '../../context'
import { createTestWorkflowStore, renderWorkflowHook } from '../../__tests__/workflow-test-env'
import { createWorkflowStore, useStore, useWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
return createTestWorkflowStore()
}
type SetterKey = keyof Shape & `set${string}`
type StateKey = Exclude<keyof Shape, SetterKey>
/**
* Verifies a simple setter → state round-trip:
* calling state[setter](value) should update state[stateKey] to equal value.
*/
function testSetter(setter: SetterKey, stateKey: StateKey, value: Shape[StateKey]) {
const store = createStore()
const setFn = store.getState()[setter] as (v: Shape[StateKey]) => void
setFn(value)
expect(store.getState()[stateKey]).toEqual(value)
}
const emptyIterParallelLogMap = new Map<string, Map<string, never[]>>()
describe('createWorkflowStore', () => {
describe('Initial State', () => {
it('should create a store with all slices merged', () => {
@ -32,60 +43,23 @@ describe('createWorkflowStore', () => {
})
describe('Workflow Slice Setters', () => {
it('should update workflowRunningData', () => {
const store = createStore()
const data: Partial<WorkflowRunningData> = { result: { status: 'running', inputs_truncated: false, process_data_truncated: false, outputs_truncated: false } }
store.getState().setWorkflowRunningData(data as Parameters<Shape['setWorkflowRunningData']>[0])
expect(store.getState().workflowRunningData).toEqual(data)
})
it('should update isListening', () => {
const store = createStore()
store.getState().setIsListening(true)
expect(store.getState().isListening).toBe(true)
})
it('should update listeningTriggerType', () => {
const store = createStore()
store.getState().setListeningTriggerType(BlockEnum.TriggerWebhook)
expect(store.getState().listeningTriggerType).toBe(BlockEnum.TriggerWebhook)
})
it('should update listeningTriggerNodeId', () => {
const store = createStore()
store.getState().setListeningTriggerNodeId('node-abc')
expect(store.getState().listeningTriggerNodeId).toBe('node-abc')
})
it('should update listeningTriggerNodeIds', () => {
const store = createStore()
store.getState().setListeningTriggerNodeIds(['n1', 'n2'])
expect(store.getState().listeningTriggerNodeIds).toEqual(['n1', 'n2'])
})
it('should update listeningTriggerIsAll', () => {
const store = createStore()
store.getState().setListeningTriggerIsAll(true)
expect(store.getState().listeningTriggerIsAll).toBe(true)
})
it('should update clipboardElements', () => {
const store = createStore()
store.getState().setClipboardElements([])
expect(store.getState().clipboardElements).toEqual([])
})
it('should update selection', () => {
const store = createStore()
const sel = { x1: 0, y1: 0, x2: 100, y2: 100 }
store.getState().setSelection(sel)
expect(store.getState().selection).toEqual(sel)
})
it('should update bundleNodeSize', () => {
const store = createStore()
store.getState().setBundleNodeSize({ width: 200, height: 100 })
expect(store.getState().bundleNodeSize).toEqual({ width: 200, height: 100 })
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['workflowRunningData', 'setWorkflowRunningData', { result: { status: 'running', inputs_truncated: false, process_data_truncated: false, outputs_truncated: false } }],
['isListening', 'setIsListening', true],
['listeningTriggerType', 'setListeningTriggerType', BlockEnum.TriggerWebhook],
['listeningTriggerNodeId', 'setListeningTriggerNodeId', 'node-abc'],
['listeningTriggerNodeIds', 'setListeningTriggerNodeIds', ['n1', 'n2']],
['listeningTriggerIsAll', 'setListeningTriggerIsAll', true],
['clipboardElements', 'setClipboardElements', []],
['selection', 'setSelection', { x1: 0, y1: 0, x2: 100, y2: 100 }],
['bundleNodeSize', 'setBundleNodeSize', { width: 200, height: 100 }],
['mousePosition', 'setMousePosition', { pageX: 10, pageY: 20, elementX: 5, elementY: 15 }],
['showConfirm', 'setShowConfirm', { title: 'Delete?', onConfirm: vi.fn() }],
['controlPromptEditorRerenderKey', 'setControlPromptEditorRerenderKey', 42],
['showImportDSLModal', 'setShowImportDSLModal', true],
['fileUploadConfig', 'setFileUploadConfig', { batch_count_limit: 5, image_file_batch_limit: 10, single_chunk_attachment_limit: 10, attachment_image_file_size_limit: 2, file_size_limit: 15, file_upload_limit: 5 }],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
it('should persist controlMode to localStorage', () => {
@ -94,180 +68,48 @@ describe('createWorkflowStore', () => {
expect(store.getState().controlMode).toBe('pointer')
expect(localStorage.setItem).toHaveBeenCalledWith('workflow-operation-mode', 'pointer')
})
it('should update mousePosition', () => {
const store = createStore()
const pos = { pageX: 10, pageY: 20, elementX: 5, elementY: 15 }
store.getState().setMousePosition(pos)
expect(store.getState().mousePosition).toEqual(pos)
})
it('should update showConfirm', () => {
const store = createStore()
const confirm = { title: 'Delete?', onConfirm: vi.fn() }
store.getState().setShowConfirm(confirm)
expect(store.getState().showConfirm).toEqual(confirm)
})
it('should update controlPromptEditorRerenderKey', () => {
const store = createStore()
store.getState().setControlPromptEditorRerenderKey(42)
expect(store.getState().controlPromptEditorRerenderKey).toBe(42)
})
it('should update showImportDSLModal', () => {
const store = createStore()
store.getState().setShowImportDSLModal(true)
expect(store.getState().showImportDSLModal).toBe(true)
})
it('should update fileUploadConfig', () => {
const store = createStore()
const config: FileUploadConfigResponse = {
batch_count_limit: 5,
image_file_batch_limit: 10,
single_chunk_attachment_limit: 10,
attachment_image_file_size_limit: 2,
file_size_limit: 15,
file_upload_limit: 5,
}
store.getState().setFileUploadConfig(config)
expect(store.getState().fileUploadConfig).toEqual(config)
})
})
describe('Node Slice Setters', () => {
it('should update showSingleRunPanel', () => {
const store = createStore()
store.getState().setShowSingleRunPanel(true)
expect(store.getState().showSingleRunPanel).toBe(true)
})
it('should update nodeAnimation', () => {
const store = createStore()
store.getState().setNodeAnimation(true)
expect(store.getState().nodeAnimation).toBe(true)
})
it('should update candidateNode', () => {
const store = createStore()
store.getState().setCandidateNode(undefined)
expect(store.getState().candidateNode).toBeUndefined()
})
it('should update nodeMenu', () => {
const store = createStore()
store.getState().setNodeMenu({ top: 100, left: 200, nodeId: 'n1' })
expect(store.getState().nodeMenu).toEqual({ top: 100, left: 200, nodeId: 'n1' })
})
it('should update showAssignVariablePopup', () => {
const store = createStore()
store.getState().setShowAssignVariablePopup(undefined)
expect(store.getState().showAssignVariablePopup).toBeUndefined()
})
it('should update hoveringAssignVariableGroupId', () => {
const store = createStore()
store.getState().setHoveringAssignVariableGroupId('group-1')
expect(store.getState().hoveringAssignVariableGroupId).toBe('group-1')
})
it('should update connectingNodePayload', () => {
const store = createStore()
const payload = { nodeId: 'n1', nodeType: 'llm', handleType: 'source', handleId: 'h1' }
store.getState().setConnectingNodePayload(payload)
expect(store.getState().connectingNodePayload).toEqual(payload)
})
it('should update enteringNodePayload', () => {
const store = createStore()
store.getState().setEnteringNodePayload(undefined)
expect(store.getState().enteringNodePayload).toBeUndefined()
})
it('should update iterTimes', () => {
const store = createStore()
store.getState().setIterTimes(5)
expect(store.getState().iterTimes).toBe(5)
})
it('should update loopTimes', () => {
const store = createStore()
store.getState().setLoopTimes(10)
expect(store.getState().loopTimes).toBe(10)
})
it('should update iterParallelLogMap', () => {
const store = createStore()
const map = new Map<string, Map<string, never[]>>()
store.getState().setIterParallelLogMap(map)
expect(store.getState().iterParallelLogMap).toBe(map)
})
it('should update pendingSingleRun', () => {
const store = createStore()
store.getState().setPendingSingleRun({ nodeId: 'n1', action: 'run' })
expect(store.getState().pendingSingleRun).toEqual({ nodeId: 'n1', action: 'run' })
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['showSingleRunPanel', 'setShowSingleRunPanel', true],
['nodeAnimation', 'setNodeAnimation', true],
['candidateNode', 'setCandidateNode', undefined],
['nodeMenu', 'setNodeMenu', { top: 100, left: 200, nodeId: 'n1' }],
['showAssignVariablePopup', 'setShowAssignVariablePopup', undefined],
['hoveringAssignVariableGroupId', 'setHoveringAssignVariableGroupId', 'group-1'],
['connectingNodePayload', 'setConnectingNodePayload', { nodeId: 'n1', nodeType: 'llm', handleType: 'source', handleId: 'h1' }],
['enteringNodePayload', 'setEnteringNodePayload', undefined],
['iterTimes', 'setIterTimes', 5],
['loopTimes', 'setLoopTimes', 10],
['iterParallelLogMap', 'setIterParallelLogMap', emptyIterParallelLogMap],
['pendingSingleRun', 'setPendingSingleRun', { nodeId: 'n1', action: 'run' }],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
})
describe('Panel Slice Setters', () => {
it('should update showFeaturesPanel', () => {
const store = createStore()
store.getState().setShowFeaturesPanel(true)
expect(store.getState().showFeaturesPanel).toBe(true)
})
it('should update showWorkflowVersionHistoryPanel', () => {
const store = createStore()
store.getState().setShowWorkflowVersionHistoryPanel(true)
expect(store.getState().showWorkflowVersionHistoryPanel).toBe(true)
})
it('should update showInputsPanel', () => {
const store = createStore()
store.getState().setShowInputsPanel(true)
expect(store.getState().showInputsPanel).toBe(true)
})
it('should update showDebugAndPreviewPanel', () => {
const store = createStore()
store.getState().setShowDebugAndPreviewPanel(true)
expect(store.getState().showDebugAndPreviewPanel).toBe(true)
})
it('should update panelMenu', () => {
const store = createStore()
store.getState().setPanelMenu({ top: 10, left: 20 })
expect(store.getState().panelMenu).toEqual({ top: 10, left: 20 })
})
it('should update selectionMenu', () => {
const store = createStore()
store.getState().setSelectionMenu({ top: 50, left: 60 })
expect(store.getState().selectionMenu).toEqual({ top: 50, left: 60 })
})
it('should update showVariableInspectPanel', () => {
const store = createStore()
store.getState().setShowVariableInspectPanel(true)
expect(store.getState().showVariableInspectPanel).toBe(true)
})
it('should update initShowLastRunTab', () => {
const store = createStore()
store.getState().setInitShowLastRunTab(true)
expect(store.getState().initShowLastRunTab).toBe(true)
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['showFeaturesPanel', 'setShowFeaturesPanel', true],
['showWorkflowVersionHistoryPanel', 'setShowWorkflowVersionHistoryPanel', true],
['showInputsPanel', 'setShowInputsPanel', true],
['showDebugAndPreviewPanel', 'setShowDebugAndPreviewPanel', true],
['panelMenu', 'setPanelMenu', { top: 10, left: 20 }],
['selectionMenu', 'setSelectionMenu', { top: 50, left: 60 }],
['showVariableInspectPanel', 'setShowVariableInspectPanel', true],
['initShowLastRunTab', 'setInitShowLastRunTab', true],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
})
describe('Help Line Slice Setters', () => {
it('should update helpLineHorizontal', () => {
const store = createStore()
const pos: HelpLineHorizontalPosition = { top: 100, left: 0, width: 500 }
store.getState().setHelpLineHorizontal(pos)
expect(store.getState().helpLineHorizontal).toEqual(pos)
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['helpLineHorizontal', 'setHelpLineHorizontal', { top: 100, left: 0, width: 500 }],
['helpLineVertical', 'setHelpLineVertical', { top: 0, left: 200, height: 300 }],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
it('should clear helpLineHorizontal', () => {
@ -276,123 +118,50 @@ describe('createWorkflowStore', () => {
store.getState().setHelpLineHorizontal(undefined)
expect(store.getState().helpLineHorizontal).toBeUndefined()
})
it('should update helpLineVertical', () => {
const store = createStore()
const pos: HelpLineVerticalPosition = { top: 0, left: 200, height: 300 }
store.getState().setHelpLineVertical(pos)
expect(store.getState().helpLineVertical).toEqual(pos)
})
})
describe('History Slice Setters', () => {
it('should update historyWorkflowData', () => {
const store = createStore()
store.getState().setHistoryWorkflowData({ id: 'run-1', status: 'succeeded' })
expect(store.getState().historyWorkflowData).toEqual({ id: 'run-1', status: 'succeeded' })
})
it('should update showRunHistory', () => {
const store = createStore()
store.getState().setShowRunHistory(true)
expect(store.getState().showRunHistory).toBe(true)
})
it('should update versionHistory', () => {
const store = createStore()
const history: VersionHistory[] = []
store.getState().setVersionHistory(history)
expect(store.getState().versionHistory).toEqual(history)
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['historyWorkflowData', 'setHistoryWorkflowData', { id: 'run-1', status: 'succeeded' }],
['showRunHistory', 'setShowRunHistory', true],
['versionHistory', 'setVersionHistory', []],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
})
describe('Form Slice Setters', () => {
it('should update inputs', () => {
const store = createStore()
store.getState().setInputs({ name: 'test', count: 42 })
expect(store.getState().inputs).toEqual({ name: 'test', count: 42 })
})
it('should update files', () => {
const store = createStore()
store.getState().setFiles([])
expect(store.getState().files).toEqual([])
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['inputs', 'setInputs', { name: 'test', count: 42 }],
['files', 'setFiles', []],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
})
describe('Tool Slice Setters', () => {
it('should update toolPublished', () => {
const store = createStore()
store.getState().setToolPublished(true)
expect(store.getState().toolPublished).toBe(true)
})
it('should update lastPublishedHasUserInput', () => {
const store = createStore()
store.getState().setLastPublishedHasUserInput(true)
expect(store.getState().lastPublishedHasUserInput).toBe(true)
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['toolPublished', 'setToolPublished', true],
['lastPublishedHasUserInput', 'setLastPublishedHasUserInput', true],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
})
describe('Layout Slice Setters', () => {
it('should update workflowCanvasWidth', () => {
const store = createStore()
store.getState().setWorkflowCanvasWidth(1200)
expect(store.getState().workflowCanvasWidth).toBe(1200)
})
it('should update workflowCanvasHeight', () => {
const store = createStore()
store.getState().setWorkflowCanvasHeight(800)
expect(store.getState().workflowCanvasHeight).toBe(800)
})
it('should update rightPanelWidth', () => {
const store = createStore()
store.getState().setRightPanelWidth(500)
expect(store.getState().rightPanelWidth).toBe(500)
})
it('should update nodePanelWidth', () => {
const store = createStore()
store.getState().setNodePanelWidth(350)
expect(store.getState().nodePanelWidth).toBe(350)
})
it('should update previewPanelWidth', () => {
const store = createStore()
store.getState().setPreviewPanelWidth(450)
expect(store.getState().previewPanelWidth).toBe(450)
})
it('should update otherPanelWidth', () => {
const store = createStore()
store.getState().setOtherPanelWidth(380)
expect(store.getState().otherPanelWidth).toBe(380)
})
it('should update bottomPanelWidth', () => {
const store = createStore()
store.getState().setBottomPanelWidth(600)
expect(store.getState().bottomPanelWidth).toBe(600)
})
it('should update bottomPanelHeight', () => {
const store = createStore()
store.getState().setBottomPanelHeight(500)
expect(store.getState().bottomPanelHeight).toBe(500)
})
it('should update variableInspectPanelHeight', () => {
const store = createStore()
store.getState().setVariableInspectPanelHeight(250)
expect(store.getState().variableInspectPanelHeight).toBe(250)
})
it('should update maximizeCanvas', () => {
const store = createStore()
store.getState().setMaximizeCanvas(true)
expect(store.getState().maximizeCanvas).toBe(true)
it.each<[StateKey, SetterKey, Shape[StateKey]]>([
['workflowCanvasWidth', 'setWorkflowCanvasWidth', 1200],
['workflowCanvasHeight', 'setWorkflowCanvasHeight', 800],
['rightPanelWidth', 'setRightPanelWidth', 500],
['nodePanelWidth', 'setNodePanelWidth', 350],
['previewPanelWidth', 'setPreviewPanelWidth', 450],
['otherPanelWidth', 'setOtherPanelWidth', 380],
['bottomPanelWidth', 'setBottomPanelWidth', 600],
['bottomPanelHeight', 'setBottomPanelHeight', 500],
['variableInspectPanelHeight', 'setVariableInspectPanelHeight', 250],
['maximizeCanvas', 'setMaximizeCanvas', true],
])('should update %s', (stateKey, setter, value) => {
testSetter(setter, stateKey, value)
})
})
@ -446,13 +215,10 @@ describe('createWorkflowStore', () => {
describe('useStore hook', () => {
it('should read state via selector when wrapped in WorkflowContext', () => {
const store = createStore()
store.getState().setShowSingleRunPanel(true)
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(WorkflowContext.Provider, { value: store }, children)
const { result } = renderHook(() => useStore(s => s.showSingleRunPanel), { wrapper })
const { result } = renderWorkflowHook(
() => useStore(s => s.showSingleRunPanel),
{ initialStoreState: { showSingleRunPanel: true } },
)
expect(result.current).toBe(true)
})
@ -465,11 +231,7 @@ describe('createWorkflowStore', () => {
describe('useWorkflowStore hook', () => {
it('should return the store instance when wrapped in WorkflowContext', () => {
const store = createStore()
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(WorkflowContext.Provider, { value: store }, children)
const { result } = renderHook(() => useWorkflowStore(), { wrapper })
const { result, store } = renderWorkflowHook(() => useWorkflowStore())
expect(result.current).toBe(store)
})
})