test(workflow): add unit tests for workflow store slices (#32932)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star 2026-03-04 10:59:31 +08:00 committed by GitHub
parent 3bf7bb1781
commit 3398962bfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1131 additions and 0 deletions

View File

@ -0,0 +1,67 @@
import type { ConversationVariable } from '@/app/components/workflow/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import { createWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
}
describe('Chat Variable Slice', () => {
describe('setShowChatVariablePanel', () => {
it('should hide other panels when opening', () => {
const store = createStore()
store.getState().setShowDebugAndPreviewPanel(true)
store.getState().setShowEnvPanel(true)
store.getState().setShowChatVariablePanel(true)
const state = store.getState()
expect(state.showChatVariablePanel).toBe(true)
expect(state.showDebugAndPreviewPanel).toBe(false)
expect(state.showEnvPanel).toBe(false)
expect(state.showGlobalVariablePanel).toBe(false)
})
it('should only close itself when setting false', () => {
const store = createStore()
store.getState().setShowChatVariablePanel(true)
store.getState().setShowChatVariablePanel(false)
expect(store.getState().showChatVariablePanel).toBe(false)
})
})
describe('setShowGlobalVariablePanel', () => {
it('should hide other panels when opening', () => {
const store = createStore()
store.getState().setShowDebugAndPreviewPanel(true)
store.getState().setShowChatVariablePanel(true)
store.getState().setShowGlobalVariablePanel(true)
const state = store.getState()
expect(state.showGlobalVariablePanel).toBe(true)
expect(state.showDebugAndPreviewPanel).toBe(false)
expect(state.showChatVariablePanel).toBe(false)
expect(state.showEnvPanel).toBe(false)
})
it('should only close itself when setting false', () => {
const store = createStore()
store.getState().setShowGlobalVariablePanel(true)
store.getState().setShowGlobalVariablePanel(false)
expect(store.getState().showGlobalVariablePanel).toBe(false)
})
})
describe('setConversationVariables', () => {
it('should update conversationVariables', () => {
const store = createStore()
const vars: ConversationVariable[] = [{ id: 'cv1', name: 'history', value: [], value_type: ChatVarType.String, description: '' }]
store.getState().setConversationVariables(vars)
expect(store.getState().conversationVariables).toEqual(vars)
})
})
})

View File

@ -0,0 +1,62 @@
import type { DataSet } from '@/models/datasets'
import { createDatasetsDetailStore } from '../../datasets-detail-store/store'
function makeDataset(id: string, name: string): DataSet {
return { id, name } as DataSet
}
describe('DatasetsDetailStore', () => {
describe('Initial State', () => {
it('should start with empty datasetsDetail', () => {
const store = createDatasetsDetailStore()
expect(store.getState().datasetsDetail).toEqual({})
})
})
describe('updateDatasetsDetail', () => {
it('should add datasets by id', () => {
const store = createDatasetsDetailStore()
const ds1 = makeDataset('ds-1', 'Dataset 1')
const ds2 = makeDataset('ds-2', 'Dataset 2')
store.getState().updateDatasetsDetail([ds1, ds2])
expect(store.getState().datasetsDetail['ds-1']).toEqual(ds1)
expect(store.getState().datasetsDetail['ds-2']).toEqual(ds2)
})
it('should merge new datasets into existing ones', () => {
const store = createDatasetsDetailStore()
const ds1 = makeDataset('ds-1', 'First')
const ds2 = makeDataset('ds-2', 'Second')
const ds3 = makeDataset('ds-3', 'Third')
store.getState().updateDatasetsDetail([ds1, ds2])
store.getState().updateDatasetsDetail([ds3])
const detail = store.getState().datasetsDetail
expect(detail['ds-1']).toEqual(ds1)
expect(detail['ds-2']).toEqual(ds2)
expect(detail['ds-3']).toEqual(ds3)
})
it('should overwrite existing datasets with same id', () => {
const store = createDatasetsDetailStore()
const ds1v1 = makeDataset('ds-1', 'Version 1')
const ds1v2 = makeDataset('ds-1', 'Version 2')
store.getState().updateDatasetsDetail([ds1v1])
store.getState().updateDatasetsDetail([ds1v2])
expect(store.getState().datasetsDetail['ds-1'].name).toBe('Version 2')
})
it('should handle empty array without errors', () => {
const store = createDatasetsDetailStore()
store.getState().updateDatasetsDetail([makeDataset('ds-1', 'Test')])
store.getState().updateDatasetsDetail([])
expect(store.getState().datasetsDetail['ds-1'].name).toBe('Test')
})
})
})

View File

@ -0,0 +1,67 @@
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { createWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
}
describe('Env Variable Slice', () => {
describe('setShowEnvPanel', () => {
it('should hide other panels when opening', () => {
const store = createStore()
store.getState().setShowDebugAndPreviewPanel(true)
store.getState().setShowChatVariablePanel(true)
store.getState().setShowEnvPanel(true)
const state = store.getState()
expect(state.showEnvPanel).toBe(true)
expect(state.showDebugAndPreviewPanel).toBe(false)
expect(state.showChatVariablePanel).toBe(false)
expect(state.showGlobalVariablePanel).toBe(false)
})
it('should only close itself when setting false', () => {
const store = createStore()
store.getState().setShowEnvPanel(true)
store.getState().setShowEnvPanel(false)
expect(store.getState().showEnvPanel).toBe(false)
})
})
describe('setEnvironmentVariables', () => {
it('should update environmentVariables', () => {
const store = createStore()
const vars: EnvironmentVariable[] = [{ id: 'v1', name: 'API_KEY', value: 'secret', value_type: 'string', description: '' }]
store.getState().setEnvironmentVariables(vars)
expect(store.getState().environmentVariables).toEqual(vars)
})
})
describe('setEnvSecrets', () => {
it('should update envSecrets', () => {
const store = createStore()
store.getState().setEnvSecrets({ API_KEY: '***' })
expect(store.getState().envSecrets).toEqual({ API_KEY: '***' })
})
})
describe('Sequential Panel Switching', () => {
it('should correctly switch between exclusive panels', () => {
const store = createStore()
store.getState().setShowChatVariablePanel(true)
expect(store.getState().showChatVariablePanel).toBe(true)
store.getState().setShowEnvPanel(true)
expect(store.getState().showEnvPanel).toBe(true)
expect(store.getState().showChatVariablePanel).toBe(false)
store.getState().setShowGlobalVariablePanel(true)
expect(store.getState().showGlobalVariablePanel).toBe(true)
expect(store.getState().showEnvPanel).toBe(false)
})
})
})

View File

@ -0,0 +1,240 @@
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
import { VarInInspectType } from '@/types/workflow'
import { createWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
}
function makeVar(overrides: Partial<VarInInspect> = {}): VarInInspect {
return {
id: 'var-1',
name: 'output',
type: VarInInspectType.node,
description: '',
selector: ['node-1', 'output'],
value_type: VarType.string,
value: 'hello',
edited: false,
visible: true,
is_truncated: false,
full_content: { size_bytes: 0, download_url: '' },
...overrides,
}
}
function makeNodeWithVar(nodeId: string, vars: VarInInspect[]): NodeWithVar {
return {
nodeId,
nodePayload: { title: `Node ${nodeId}`, desc: '', type: BlockEnum.Code } as NodeWithVar['nodePayload'],
nodeType: BlockEnum.Code,
title: `Node ${nodeId}`,
vars,
isValueFetched: false,
}
}
describe('Inspect Vars Slice', () => {
describe('setNodesWithInspectVars', () => {
it('should replace the entire list', () => {
const store = createStore()
const nodes = [makeNodeWithVar('n1', [makeVar()])]
store.getState().setNodesWithInspectVars(nodes)
expect(store.getState().nodesWithInspectVars).toEqual(nodes)
})
})
describe('deleteAllInspectVars', () => {
it('should clear all nodes', () => {
const store = createStore()
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [makeVar()])])
store.getState().deleteAllInspectVars()
expect(store.getState().nodesWithInspectVars).toEqual([])
})
})
describe('setNodeInspectVars', () => {
it('should update vars for a specific node and mark as fetched', () => {
const store = createStore()
const v1 = makeVar({ id: 'v1', name: 'a' })
const v2 = makeVar({ id: 'v2', name: 'b' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v1])])
store.getState().setNodeInspectVars('n1', [v2])
const node = store.getState().nodesWithInspectVars[0]
expect(node.vars).toEqual([v2])
expect(node.isValueFetched).toBe(true)
})
it('should not modify state when node is not found', () => {
const store = createStore()
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [makeVar()])])
store.getState().setNodeInspectVars('non-existent', [])
expect(store.getState().nodesWithInspectVars[0].vars).toHaveLength(1)
})
})
describe('deleteNodeInspectVars', () => {
it('should remove the matching node', () => {
const store = createStore()
store.getState().setNodesWithInspectVars([
makeNodeWithVar('n1', [makeVar()]),
makeNodeWithVar('n2', [makeVar()]),
])
store.getState().deleteNodeInspectVars('n1')
expect(store.getState().nodesWithInspectVars).toHaveLength(1)
expect(store.getState().nodesWithInspectVars[0].nodeId).toBe('n2')
})
})
describe('setInspectVarValue', () => {
it('should update the value and set edited=true', () => {
const store = createStore()
const v = makeVar({ id: 'v1', value: 'old', edited: false })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().setInspectVarValue('n1', 'v1', 'new')
const updated = store.getState().nodesWithInspectVars[0].vars[0]
expect(updated.value).toBe('new')
expect(updated.edited).toBe(true)
})
it('should not change state when var is not found', () => {
const store = createStore()
const v = makeVar({ id: 'v1', value: 'old' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().setInspectVarValue('n1', 'wrong-id', 'new')
expect(store.getState().nodesWithInspectVars[0].vars[0].value).toBe('old')
})
it('should not change state when node is not found', () => {
const store = createStore()
const v = makeVar({ id: 'v1', value: 'old' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().setInspectVarValue('wrong-node', 'v1', 'new')
expect(store.getState().nodesWithInspectVars[0].vars[0].value).toBe('old')
})
})
describe('resetToLastRunVar', () => {
it('should restore value and set edited=false', () => {
const store = createStore()
const v = makeVar({ id: 'v1', value: 'modified', edited: true })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().resetToLastRunVar('n1', 'v1', 'original')
const updated = store.getState().nodesWithInspectVars[0].vars[0]
expect(updated.value).toBe('original')
expect(updated.edited).toBe(false)
})
it('should not change state when node is not found', () => {
const store = createStore()
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [makeVar()])])
store.getState().resetToLastRunVar('wrong-node', 'v1', 'val')
expect(store.getState().nodesWithInspectVars[0].vars[0].edited).toBe(false)
})
it('should not change state when var is not found', () => {
const store = createStore()
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [makeVar({ id: 'v1', edited: true })])])
store.getState().resetToLastRunVar('n1', 'wrong-var', 'val')
expect(store.getState().nodesWithInspectVars[0].vars[0].edited).toBe(true)
})
})
describe('renameInspectVarName', () => {
it('should update name and selector', () => {
const store = createStore()
const v = makeVar({ id: 'v1', name: 'old_name', selector: ['n1', 'old_name'] })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().renameInspectVarName('n1', 'v1', ['n1', 'new_name'])
const updated = store.getState().nodesWithInspectVars[0].vars[0]
expect(updated.name).toBe('new_name')
expect(updated.selector).toEqual(['n1', 'new_name'])
})
it('should not change state when node is not found', () => {
const store = createStore()
const v = makeVar({ id: 'v1', name: 'old' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().renameInspectVarName('wrong-node', 'v1', ['x', 'y'])
expect(store.getState().nodesWithInspectVars[0].vars[0].name).toBe('old')
})
it('should not change state when var is not found', () => {
const store = createStore()
const v = makeVar({ id: 'v1', name: 'old' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().renameInspectVarName('n1', 'wrong-var', ['x', 'y'])
expect(store.getState().nodesWithInspectVars[0].vars[0].name).toBe('old')
})
})
describe('deleteInspectVar', () => {
it('should remove the matching var from the node', () => {
const store = createStore()
const v1 = makeVar({ id: 'v1' })
const v2 = makeVar({ id: 'v2' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v1, v2])])
store.getState().deleteInspectVar('n1', 'v1')
const vars = store.getState().nodesWithInspectVars[0].vars
expect(vars).toHaveLength(1)
expect(vars[0].id).toBe('v2')
})
it('should not change state when var is not found', () => {
const store = createStore()
const v = makeVar({ id: 'v1' })
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [v])])
store.getState().deleteInspectVar('n1', 'wrong-id')
expect(store.getState().nodesWithInspectVars[0].vars).toHaveLength(1)
})
it('should not change state when node is not found', () => {
const store = createStore()
store.getState().setNodesWithInspectVars([makeNodeWithVar('n1', [makeVar()])])
store.getState().deleteInspectVar('wrong-node', 'v1')
expect(store.getState().nodesWithInspectVars[0].vars).toHaveLength(1)
})
})
describe('currentFocusNodeId', () => {
it('should update and clear focus node', () => {
const store = createStore()
store.getState().setCurrentFocusNodeId('n1')
expect(store.getState().currentFocusNodeId).toBe('n1')
store.getState().setCurrentFocusNodeId(null)
expect(store.getState().currentFocusNodeId).toBeNull()
})
})
})

View File

@ -0,0 +1,43 @@
import type { Dependency } from '@/app/components/plugins/types'
import { useStore } from '../../plugin-dependency/store'
describe('Plugin Dependency Store', () => {
beforeEach(() => {
useStore.setState({ dependencies: [] })
})
describe('Initial State', () => {
it('should start with empty dependencies', () => {
expect(useStore.getState().dependencies).toEqual([])
})
})
describe('setDependencies', () => {
it('should update dependencies list', () => {
const deps: Dependency[] = [
{ type: 'marketplace', value: { plugin_unique_identifier: 'p1' } },
{ type: 'marketplace', value: { plugin_unique_identifier: 'p2' } },
] as Dependency[]
useStore.getState().setDependencies(deps)
expect(useStore.getState().dependencies).toEqual(deps)
})
it('should replace existing dependencies', () => {
const dep1: Dependency = { type: 'marketplace', value: { plugin_unique_identifier: 'p1' } } as Dependency
const dep2: Dependency = { type: 'marketplace', value: { plugin_unique_identifier: 'p2' } } as Dependency
useStore.getState().setDependencies([dep1])
useStore.getState().setDependencies([dep2])
expect(useStore.getState().dependencies).toHaveLength(1)
})
it('should handle empty array', () => {
const dep: Dependency = { type: 'marketplace', value: { plugin_unique_identifier: 'p1' } } as Dependency
useStore.getState().setDependencies([dep])
useStore.getState().setDependencies([])
expect(useStore.getState().dependencies).toEqual([])
})
})
})

View File

@ -0,0 +1,61 @@
import type { VersionHistory } from '@/types/workflow'
import { createWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
}
describe('Version Slice', () => {
describe('setDraftUpdatedAt', () => {
it('should multiply timestamp by 1000 (seconds to milliseconds)', () => {
const store = createStore()
store.getState().setDraftUpdatedAt(1704067200)
expect(store.getState().draftUpdatedAt).toBe(1704067200000)
})
it('should set 0 when given 0', () => {
const store = createStore()
store.getState().setDraftUpdatedAt(0)
expect(store.getState().draftUpdatedAt).toBe(0)
})
})
describe('setPublishedAt', () => {
it('should multiply timestamp by 1000', () => {
const store = createStore()
store.getState().setPublishedAt(1704067200)
expect(store.getState().publishedAt).toBe(1704067200000)
})
it('should set 0 when given 0', () => {
const store = createStore()
store.getState().setPublishedAt(0)
expect(store.getState().publishedAt).toBe(0)
})
})
describe('currentVersion', () => {
it('should default to null', () => {
const store = createStore()
expect(store.getState().currentVersion).toBeNull()
})
it('should update current version', () => {
const store = createStore()
const version = { hash: 'abc', updated_at: 1000, version: '1.0' } as VersionHistory
store.getState().setCurrentVersion(version)
expect(store.getState().currentVersion).toEqual(version)
})
})
describe('isRestoring', () => {
it('should toggle restoring state', () => {
const store = createStore()
store.getState().setIsRestoring(true)
expect(store.getState().isRestoring).toBe(true)
store.getState().setIsRestoring(false)
expect(store.getState().isRestoring).toBe(false)
})
})
})

View File

@ -0,0 +1,105 @@
import type { Node } from '@/app/components/workflow/types'
import { createWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
}
describe('Workflow Draft Slice', () => {
describe('Initial State', () => {
it('should have empty default values', () => {
const store = createStore()
const state = store.getState()
expect(state.backupDraft).toBeUndefined()
expect(state.syncWorkflowDraftHash).toBe('')
expect(state.isSyncingWorkflowDraft).toBe(false)
expect(state.isWorkflowDataLoaded).toBe(false)
expect(state.nodes).toEqual([])
})
})
describe('setBackupDraft', () => {
it('should set and clear backup draft', () => {
const store = createStore()
const draft = {
nodes: [] as Node[],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
environmentVariables: [],
}
store.getState().setBackupDraft(draft)
expect(store.getState().backupDraft).toEqual(draft)
store.getState().setBackupDraft(undefined)
expect(store.getState().backupDraft).toBeUndefined()
})
})
describe('setSyncWorkflowDraftHash', () => {
it('should update the hash', () => {
const store = createStore()
store.getState().setSyncWorkflowDraftHash('abc123')
expect(store.getState().syncWorkflowDraftHash).toBe('abc123')
})
})
describe('setIsSyncingWorkflowDraft', () => {
it('should toggle syncing state', () => {
const store = createStore()
store.getState().setIsSyncingWorkflowDraft(true)
expect(store.getState().isSyncingWorkflowDraft).toBe(true)
})
})
describe('setIsWorkflowDataLoaded', () => {
it('should toggle loaded state', () => {
const store = createStore()
store.getState().setIsWorkflowDataLoaded(true)
expect(store.getState().isWorkflowDataLoaded).toBe(true)
})
})
describe('setNodes', () => {
it('should update nodes array', () => {
const store = createStore()
const nodes: Node[] = []
store.getState().setNodes(nodes)
expect(store.getState().nodes).toEqual(nodes)
})
})
describe('debouncedSyncWorkflowDraft', () => {
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()
store.getState().debouncedSyncWorkflowDraft(syncFn)
expect(syncFn).not.toHaveBeenCalled()
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()
store.getState().debouncedSyncWorkflowDraft(syncFn)
expect(syncFn).not.toHaveBeenCalled()
store.getState().flushPendingSync()
expect(syncFn).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
})
})

View File

@ -0,0 +1,486 @@
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 { createWorkflowStore, useStore, useWorkflowStore } from '../workflow'
function createStore() {
return createWorkflowStore({})
}
describe('createWorkflowStore', () => {
describe('Initial State', () => {
it('should create a store with all slices merged', () => {
const store = createStore()
const state = store.getState()
expect(state.showSingleRunPanel).toBe(false)
expect(state.controlMode).toBeDefined()
expect(state.nodes).toEqual([])
expect(state.environmentVariables).toEqual([])
expect(state.conversationVariables).toEqual([])
expect(state.nodesWithInspectVars).toEqual([])
expect(state.workflowCanvasWidth).toBeUndefined()
expect(state.draftUpdatedAt).toBe(0)
expect(state.versionHistory).toEqual([])
})
})
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('should persist controlMode to localStorage', () => {
const store = createStore()
store.getState().setControlMode('pointer')
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' })
})
})
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)
})
})
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('should clear helpLineHorizontal', () => {
const store = createStore()
store.getState().setHelpLineHorizontal({ top: 100, left: 0, width: 500 })
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)
})
})
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([])
})
})
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)
})
})
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)
})
})
describe('localStorage Initialization', () => {
it('should read controlMode from localStorage', () => {
localStorage.setItem('workflow-operation-mode', 'pointer')
const store = createStore()
expect(store.getState().controlMode).toBe('pointer')
})
it('should default controlMode to hand when localStorage has no value', () => {
const store = createStore()
expect(store.getState().controlMode).toBe('hand')
})
it('should read panelWidth from localStorage', () => {
localStorage.setItem('workflow-node-panel-width', '500')
const store = createStore()
expect(store.getState().panelWidth).toBe(500)
})
it('should default panelWidth to 420 when localStorage is empty', () => {
const store = createStore()
expect(store.getState().panelWidth).toBe(420)
})
it('should read nodePanelWidth from localStorage', () => {
localStorage.setItem('workflow-node-panel-width', '350')
const store = createStore()
expect(store.getState().nodePanelWidth).toBe(350)
})
it('should read previewPanelWidth from localStorage', () => {
localStorage.setItem('debug-and-preview-panel-width', '450')
const store = createStore()
expect(store.getState().previewPanelWidth).toBe(450)
})
it('should read variableInspectPanelHeight from localStorage', () => {
localStorage.setItem('workflow-variable-inpsect-panel-height', '200')
const store = createStore()
expect(store.getState().variableInspectPanelHeight).toBe(200)
})
it('should read maximizeCanvas from localStorage', () => {
localStorage.setItem('workflow-canvas-maximize', 'true')
const store = createStore()
expect(store.getState().maximizeCanvas).toBe(true)
})
})
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 })
expect(result.current).toBe(true)
})
it('should throw when used without WorkflowContext.Provider', () => {
expect(() => {
renderHook(() => useStore(s => s.showSingleRunPanel))
}).toThrow('Missing WorkflowContext.Provider in the tree')
})
})
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 })
expect(result.current).toBe(store)
})
})
describe('Injection', () => {
it('should support injecting additional slice', () => {
const injected: SliceFromInjection = {}
const store = createWorkflowStore({
injectWorkflowStoreSliceFn: () => injected,
})
expect(store.getState()).toBeDefined()
})
})
})