, isRetrievalModeChange?: boolean) => void }) => (
+
+
+
+
+ ),
+}))
+
+vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal', () => ({
+ __esModule: true,
+ default: () => model-parameter-modal
,
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-vars', () => ({
+ __esModule: true,
+ default: ({ onChange }: { onChange: (valueSelector: string[], varItem: { type: VarType }) => void }) => (
+
+ ),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable-tag', () => ({
+ __esModule: true,
+ default: ({ valueSelector }: { valueSelector: string[] }) => {valueSelector.join('.')}
,
+}))
+
+vi.mock('../components/metadata/metadata-panel', () => ({
+ __esModule: true,
+ default: ({ onCancel }: { onCancel: () => void }) => (
+
+
metadata-panel
+
+
+ ),
+}))
+
+describe('knowledge-retrieval path', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockHasEditPermissionForDataset.mockReturnValue(true)
+ })
+
+ describe('Dataset controls', () => {
+ it('should open dataset selector and forward selected datasets', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+
+ render(
+ ,
+ )
+
+ await user.click(screen.getByTestId('add-button'))
+ await user.click(screen.getByText('select-dataset'))
+
+ expect(onChange).toHaveBeenCalledWith([
+ expect.objectContaining({
+ id: 'dataset-2',
+ name: 'Selected Dataset',
+ }),
+ ])
+ })
+
+ it('should support editing and removing a dataset item', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+ const onRemove = vi.fn()
+
+ render(
+ ,
+ )
+
+ expect(screen.getByText('Dataset Name')).toBeInTheDocument()
+ fireEvent.mouseOver(screen.getByText('Dataset Name').closest('.group\\/dataset-item')!)
+
+ const buttons = screen.getAllByRole('button')
+ await user.click(buttons[0]!)
+ await user.click(screen.getByText('save-settings'))
+ await user.click(buttons[1]!)
+
+ expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ name: 'Updated Dataset' }))
+ expect(onRemove).toHaveBeenCalled()
+ })
+
+ it('should render empty and populated dataset lists', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+
+ const { rerender } = render(
+ ,
+ )
+
+ expect(screen.getByText('appDebug.datasetConfig.knowledgeTip')).toBeInTheDocument()
+
+ rerender(
+ ,
+ )
+
+ fireEvent.mouseOver(screen.getByText('Dataset Name').closest('.group\\/dataset-item')!)
+ await user.click(screen.getAllByRole('button')[1]!)
+
+ expect(onChange).toHaveBeenCalledWith([])
+ })
+ })
+
+ describe('Retrieval settings', () => {
+ it('should open retrieval config and map config updates back to workflow payload', async () => {
+ const user = userEvent.setup()
+ const onRetrievalModeChange = vi.fn()
+ const onMultipleRetrievalConfigChange = vi.fn()
+
+ render(
+ ,
+ )
+
+ await user.click(screen.getByText('apply-retrieval-config'))
+ await user.click(screen.getByText('change-retrieval-mode'))
+
+ expect(onMultipleRetrievalConfigChange).toHaveBeenCalledWith(expect.objectContaining({
+ top_k: 8,
+ score_threshold: 0.4,
+ reranking_model: {
+ provider: 'cohere',
+ model: 'rerank-v3',
+ },
+ reranking_enable: true,
+ }))
+ expect(onRetrievalModeChange).toHaveBeenCalledWith(RETRIEVE_TYPE.oneWay)
+ })
+ })
+
+ describe('Metadata controls', () => {
+ it('should select metadata filter mode from the dropdown', async () => {
+ const user = userEvent.setup()
+ const onSelect = vi.fn()
+
+ render(
+ ,
+ )
+
+ await user.click(screen.getByRole('button', { name: /workflow.nodes.knowledgeRetrieval.metadata.options.disabled.title/i }))
+ await user.click(screen.getByText('workflow.nodes.knowledgeRetrieval.metadata.options.manual.title'))
+
+ expect(onSelect).toHaveBeenCalledWith(MetadataFilteringModeEnum.manual)
+ })
+
+ it('should remove stale metadata conditions and open the manual metadata panel', async () => {
+ const user = userEvent.setup()
+ const handleRemoveCondition = vi.fn()
+
+ render(
+ ,
+ )
+
+ expect(handleRemoveCondition).toHaveBeenCalledWith('condition-stale')
+
+ await user.click(screen.getByRole('button', { name: /workflow.nodes.knowledgeRetrieval.metadata.panel.conditions/i }))
+
+ expect(screen.getByText('metadata-panel')).toBeInTheDocument()
+ })
+
+ it('should render automatic and manual metadata filter states', async () => {
+ const user = userEvent.setup()
+ const baseProps: MetadataShape = {
+ metadataList: [createMetadata()],
+ metadataFilteringConditions: {
+ logical_operator: LogicalOperator.and,
+ conditions: [createCondition()],
+ },
+ selectedDatasetsLoaded: true,
+ handleAddCondition: vi.fn(),
+ handleRemoveCondition: vi.fn(),
+ handleToggleConditionLogicalOperator: vi.fn(),
+ handleUpdateCondition: vi.fn(),
+ }
+
+ const { rerender } = render(
+ ,
+ )
+
+ expect(screen.getByRole('button', { name: /workflow.nodes.knowledgeRetrieval.metadata.options.automatic.title/i })).toBeInTheDocument()
+
+ rerender(
+ ,
+ )
+
+ await user.click(screen.getByRole('button', { name: /workflow.nodes.knowledgeRetrieval.metadata.panel.conditions/i }))
+
+ expect(screen.getByText('metadata-panel')).toBeInTheDocument()
+ })
+ })
+
+ describe('Condition inputs', () => {
+ it('should toggle value method and keep the same option idempotent', async () => {
+ const user = userEvent.setup()
+ const onValueMethodChange = vi.fn()
+
+ render(
+ ,
+ )
+
+ await user.click(screen.getByRole('button', { name: /variable/i }))
+ await user.click(screen.getByText('Constant'))
+ await user.click(screen.getByRole('button', { name: /variable/i }))
+ await user.click(screen.getAllByText('Variable')[1]!)
+
+ expect(onValueMethodChange).toHaveBeenCalledTimes(1)
+ expect(onValueMethodChange).toHaveBeenCalledWith('constant')
+ })
+
+ it('should select workflow and common variables', async () => {
+ const user = userEvent.setup()
+ const onVariableChange = vi.fn()
+ const onCommonVariableChange = vi.fn()
+
+ const { rerender } = render(
+ ,
+ )
+
+ await user.click(screen.getByText('workflow.nodes.knowledgeRetrieval.metadata.panel.select'))
+ await user.click(screen.getByText('pick-var'))
+
+ expect(onVariableChange).toHaveBeenCalledWith(['node-1', 'field'], { type: VarType.string })
+
+ rerender(
+ ,
+ )
+
+ await user.click(screen.getByText('workflow.nodes.knowledgeRetrieval.metadata.panel.select'))
+ await user.click(screen.getByText('sys.user_name'))
+
+ expect(onCommonVariableChange).toHaveBeenCalledWith('sys.user_name')
+ })
+
+ it('should update operator, clear date values, and remove conditions', async () => {
+ const user = userEvent.setup()
+ const onSelect = vi.fn()
+ const onDateChange = vi.fn()
+ const onRemoveCondition = vi.fn()
+ const onUpdateCondition = vi.fn()
+
+ const { container } = render(
+
+
+
+
+
,
+ )
+
+ await user.click(screen.getAllByRole('button', { name: /contains/i })[0]!)
+ await user.click(screen.getByText('workflow.nodes.ifElse.comparisonOperator.is'))
+ await user.click(screen.getByText(/March 09 2024/).nextElementSibling as Element)
+ fireEvent.change(screen.getByDisplayValue('agent'), { target: { value: 'updated-agent' } })
+ fireEvent.click(container.querySelector('.ml-1.mt-1') as Element)
+
+ expect(onSelect).toHaveBeenCalledWith(MetadataComparisonOperator.is as ComparisonOperator)
+ expect(onDateChange).toHaveBeenCalledWith()
+ expect(onUpdateCondition).toHaveBeenCalledWith('condition-1', expect.objectContaining({ value: 'updated-agent' }))
+ expect(onRemoveCondition).toHaveBeenCalledWith('condition-1')
+ })
+ })
+
+ describe('Node rendering', () => {
+ it('should render selected datasets from the detail store and hide when none are selected', () => {
+ const store = createDatasetsDetailStore()
+ store.getState().updateDatasetsDetail([createDataset()])
+
+ const renderNode = (datasetIds: string[]) => render(
+
+
+ ,
+ )
+
+ const { rerender, container } = renderNode(['dataset-1'])
+
+ expect(screen.getByText('Dataset Name')).toBeInTheDocument()
+
+ rerender(
+
+
+ ,
+ )
+
+ expect(container).toBeEmptyDOMElement()
+ })
+ })
+})
diff --git a/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx
new file mode 100644
index 0000000000..4f443f293f
--- /dev/null
+++ b/web/app/components/workflow/nodes/list-operator/__tests__/integration.spec.tsx
@@ -0,0 +1,309 @@
+/* eslint-disable ts/no-explicit-any, style/jsx-one-expression-per-line */
+import type { ListFilterNodeType } from '../types'
+import type { PanelProps } from '@/types/workflow'
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
+import { BlockEnum, VarType } from '@/app/components/workflow/types'
+import ExtractInput from '../components/extract-input'
+import LimitConfig from '../components/limit-config'
+import SubVariablePicker from '../components/sub-variable-picker'
+import Node from '../node'
+import Panel from '../panel'
+import { OrderBy } from '../types'
+import useConfig from '../use-config'
+
+vi.mock('@/app/components/workflow/nodes/_base/hooks/use-available-var-list', () => ({
+ default: vi.fn((_nodeId: string, options?: any) => ({
+ availableVars: [
+ { variable: ['node-1', 'size'], type: VarType.number },
+ { variable: ['node-1', 'name'], type: VarType.string },
+ ].filter(varPayload => options?.filterVar ? options.filterVar(varPayload) : true),
+ availableNodesWithParent: [{ id: 'node-1', data: { title: 'Answer', type: BlockEnum.Answer } }],
+ })),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/input-support-select-var', () => ({
+ default: ({ value, onChange, placeholder, className, readOnly, onFocusChange }: any) => (
+ onFocusChange?.(true)}
+ onBlur={() => onFocusChange?.(false)}
+ onChange={event => onChange(event.target.value)}
+ />
+ ),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({
+ default: ({ title, operations, children }: any) => {title}
{operations}
{children}
,
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/input-number-with-slider', () => ({
+ default: ({ value, onChange }: any) => (
+
+ ),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/option-card', () => ({
+ default: ({ title, onSelect }: any) => ,
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/output-vars', () => ({
+ default: ({ children }: any) => {children}
,
+ VarItem: ({ name, type }: any) => {name}:{type}
,
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/split', () => ({
+ default: () => split
,
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
+ default: ({ onChange }: any) => ,
+}))
+
+vi.mock('../components/filter-condition', () => ({
+ default: ({ onChange }: any) => ,
+}))
+
+vi.mock('../use-config', () => ({
+ default: vi.fn(),
+}))
+
+const mockUseConfig = vi.mocked(useConfig)
+
+const createData = (overrides: Partial = {}): ListFilterNodeType => ({
+ title: 'List Operator',
+ desc: '',
+ type: BlockEnum.ListFilter,
+ variable: ['node-1', 'items'],
+ var_type: VarType.arrayNumber,
+ item_var_type: VarType.number,
+ filter_by: { enabled: true, conditions: [{ key: 'size', comparison_operator: 'equal', value: '1' }] as any },
+ extract_by: { enabled: true, serial: '1' },
+ limit: { enabled: true, size: 10 },
+ order_by: { enabled: true, key: 'size', value: OrderBy.ASC },
+ ...overrides,
+})
+
+const createConfigResult = (overrides: Partial> = {}): ReturnType => ({
+ readOnly: false,
+ inputs: createData(),
+ filterVar: vi.fn(() => true),
+ varType: VarType.arrayNumber,
+ itemVarType: VarType.number,
+ itemVarTypeShowName: 'number',
+ hasSubVariable: true,
+ handleVarChanges: vi.fn(),
+ handleFilterEnabledChange: vi.fn(),
+ handleFilterChange: vi.fn(),
+ handleLimitChange: vi.fn(),
+ handleOrderByEnabledChange: vi.fn(),
+ handleOrderByKeyChange: vi.fn(),
+ handleOrderByTypeChange: vi.fn(() => vi.fn()),
+ handleExtractsEnabledChange: vi.fn(),
+ handleExtractsChange: vi.fn(),
+ ...overrides,
+})
+
+const panelProps: PanelProps = {
+ getInputVars: vi.fn(() => []),
+ toVarInputs: vi.fn(() => []),
+ runInputData: {},
+ runInputDataRef: { current: {} },
+ setRunInputData: vi.fn(),
+ runResult: null,
+}
+
+const renderPanel = (data: ListFilterNodeType = createData()) => (
+ render()
+)
+
+describe('list-operator path', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mockUseConfig.mockReturnValue(createConfigResult())
+ })
+
+ // The list-operator path should expose extract, limit, ordering, and node variable previews.
+ describe('Path Integration', () => {
+ it('should update the extract input', async () => {
+ const onChange = vi.fn()
+ const { rerender } = render(
+ ,
+ )
+
+ fireEvent.change(screen.getByDisplayValue('1'), { target: { value: '2' } })
+ fireEvent.focus(screen.getByDisplayValue('1'))
+ expect(screen.getByDisplayValue('1')).toHaveClass('border-components-input-border-active')
+
+ rerender(
+ ,
+ )
+
+ expect(onChange).toHaveBeenCalled()
+ expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', '')
+ })
+
+ it('should change the selected sub variable', async () => {
+ const onChange = vi.fn()
+ const { unmount } = render(
+ ,
+ )
+
+ const trigger = screen.getByRole('button')
+
+ await act(async () => {
+ fireEvent.keyDown(trigger, { key: 'ArrowDown' })
+ })
+
+ const option = await screen.findByText('name')
+ await act(async () => {
+ fireEvent.click(option)
+ })
+
+ await waitFor(() => {
+ expect(onChange).toHaveBeenCalledWith('name')
+ })
+
+ unmount()
+ render(
+ ,
+ )
+
+ expect(screen.getByText('common.placeholder.select')).toBeInTheDocument()
+ })
+
+ it('should toggle limit and update the size slider', async () => {
+ const user = userEvent.setup()
+ const onChange = vi.fn()
+ const { rerender } = render(
+ ,
+ )
+
+ await user.click(screen.getByText('slider-10'))
+
+ expect(onChange).toHaveBeenCalledWith({ enabled: true, size: 11 })
+
+ rerender(
+ ,
+ )
+
+ expect(screen.queryByText('slider-10')).not.toBeInTheDocument()
+ await user.click(screen.getByRole('switch'))
+ expect(onChange).toHaveBeenCalledWith({ enabled: true, size: 10 })
+ })
+
+ it('should render the selected input variable in the node preview', () => {
+ renderWorkflowFlowComponent(
+ ,
+ {
+ nodes: [{ id: 'node-1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Answer, title: 'Answer' } as any }],
+ edges: [],
+ },
+ )
+
+ expect(screen.getByText('Answer')).toBeInTheDocument()
+ expect(screen.getByText('items')).toBeInTheDocument()
+ })
+
+ it('should resolve system variables through the start node and return null without a variable', () => {
+ const { rerender } = renderWorkflowFlowComponent(
+ ,
+ {
+ nodes: [{ id: 'start', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, title: 'Start' } as any }],
+ edges: [],
+ },
+ )
+
+ expect(screen.getByText('Start')).toBeInTheDocument()
+
+ rerender(
+ ,
+ )
+
+ expect(screen.queryByText('workflow.nodes.listFilter.inputVar')).not.toBeInTheDocument()
+ expect(screen.queryByText('Start')).not.toBeInTheDocument()
+ })
+
+ it('should render the panel controls and output vars', async () => {
+ const user = userEvent.setup()
+ renderPanel()
+
+ await user.click(screen.getByText('pick-var'))
+ await user.click(screen.getByText('filter-condition'))
+ await user.click(screen.getByText('workflow.nodes.listFilter.asc'))
+
+ expect(screen.getByText('result:Array[number]')).toBeInTheDocument()
+ expect(screen.getByText('first_record:number')).toBeInTheDocument()
+ expect(screen.getByText('last_record:number')).toBeInTheDocument()
+ })
+
+ it('should hide disabled sections and render order controls without sub variables', () => {
+ mockUseConfig.mockReturnValueOnce(createConfigResult({
+ inputs: createData({
+ variable: undefined as any,
+ filter_by: { enabled: false, conditions: [] as any },
+ extract_by: { enabled: false, serial: '' },
+ order_by: { enabled: false, key: '', value: OrderBy.ASC },
+ }),
+ hasSubVariable: false,
+ }))
+
+ const { rerender } = renderPanel()
+
+ expect(screen.queryByText('filter-condition')).not.toBeInTheDocument()
+ expect(screen.queryByDisplayValue('1')).not.toBeInTheDocument()
+ expect(screen.queryByText('workflow.nodes.listFilter.asc')).not.toBeInTheDocument()
+
+ mockUseConfig.mockReturnValueOnce(createConfigResult({
+ inputs: createData({
+ order_by: { enabled: true, key: '', value: OrderBy.ASC },
+ }),
+ hasSubVariable: false,
+ }))
+
+ rerender()
+
+ expect(screen.getByText('workflow.nodes.listFilter.asc')).toBeInTheDocument()
+ expect(screen.queryByText('common.placeholder.select')).not.toBeInTheDocument()
+ })
+ })
+})
diff --git a/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx
index ee4891cfa3..42eff04e68 100644
--- a/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx
+++ b/web/app/components/workflow/nodes/llm/__tests__/panel.spec.tsx
@@ -1,10 +1,8 @@
import type { LLMNodeType } from '../types'
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
-import type { ProviderContextState } from '@/context/provider-context'
import type { PanelProps } from '@/types/workflow'
-import { render, screen } from '@testing-library/react'
-import * as React from 'react'
-import { defaultPlan } from '@/app/components/billing/config'
+import { screen } from '@testing-library/react'
+import { createMockProviderContextValue } from '@/__mocks__/provider-context'
import {
ConfigurationMethodEnum,
CurrentSystemQuotaTypeEnum,
@@ -12,17 +10,14 @@ import {
ModelTypeEnum,
PreferredProviderTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
-import { useProviderContextSelector } from '@/context/provider-context'
+import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
+import { ProviderContext } from '@/context/provider-context'
import { AppModeEnum } from '@/types/app'
import { BlockEnum } from '../../../types'
import Panel from '../panel'
const mockUseConfig = vi.fn()
-vi.mock('@/context/provider-context', () => ({
- useProviderContextSelector: vi.fn(),
-}))
-
vi.mock('../use-config', () => ({
default: (...args: unknown[]) => mockUseConfig(...args),
}))
@@ -31,80 +26,12 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-param
default: () => ,
}))
-vi.mock('../components/config-prompt', () => ({
- default: () => ,
-}))
-
-vi.mock('../../_base/components/config-vision', () => ({
- default: () => null,
-}))
-
-vi.mock('../../_base/components/memory-config', () => ({
- default: () => null,
-}))
-
vi.mock('../../_base/components/variable/var-reference-picker', () => ({
default: () => null,
}))
-vi.mock('@/app/components/workflow/nodes/_base/components/prompt/editor', () => ({
- default: () => null,
-}))
-
-vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-list', () => ({
- default: () => null,
-}))
-
-vi.mock('../components/reasoning-format-config', () => ({
- default: () => null,
-}))
-
-vi.mock('../components/structure-output', () => ({
- default: () => null,
-}))
-
-vi.mock('@/app/components/workflow/nodes/_base/components/output-vars', () => ({
- default: ({ children }: { children?: React.ReactNode }) => {children}
,
- VarItem: () => null,
-}))
-
type MockUseConfigReturn = ReturnType
-const modelProviderSelector = vi.mocked(useProviderContextSelector)
-
-const createProviderContextState = (modelProviders: ModelProvider[]): ProviderContextState => ({
- modelProviders,
- refreshModelProviders: vi.fn(),
- textGenerationModelList: [],
- supportRetrievalMethods: [],
- isAPIKeySet: true,
- plan: defaultPlan,
- isFetchedPlan: true,
- enableBilling: false,
- onPlanInfoChanged: vi.fn(),
- enableReplaceWebAppLogo: false,
- modelLoadBalancingEnabled: false,
- datasetOperatorEnabled: false,
- enableEducationPlan: false,
- isEducationWorkspace: false,
- isEducationAccount: false,
- allowRefreshEducationVerify: false,
- educationAccountExpireAt: null,
- isLoadingEducationAccountInfo: false,
- isFetchingEducationAccountInfo: false,
- webappCopyrightEnabled: false,
- licenseLimit: {
- workspace_members: {
- size: 0,
- limit: 0,
- },
- },
- refreshLicenseLimit: vi.fn(),
- isAllowTransferWorkspace: false,
- isAllowPublishAsCustomKnowledgePipelineTemplate: false,
- humanInputEmailDeliveryEnabled: false,
-})
-
const createMockModelProvider = (provider: string): ModelProvider => ({
provider,
label: { en_US: provider, zh_Hans: provider },
@@ -195,21 +122,27 @@ const buildUseConfigResult = (overrides?: Partial) => ({
})
const renderPanel = (data?: Partial) => {
- return render(
- ,
+ return renderWorkflowFlowComponent(
+
+
+ ,
+ {
+ hooksStoreProps: {},
+ },
)
}
describe('LLM Panel', () => {
beforeEach(() => {
vi.clearAllMocks()
- modelProviderSelector.mockImplementation(selector => selector(
- createProviderContextState([createMockModelProvider('openai')]),
- ))
mockUseConfig.mockReturnValue(buildUseConfigResult())
})
diff --git a/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx
new file mode 100644
index 0000000000..10b8dad885
--- /dev/null
+++ b/web/app/components/workflow/nodes/loop/__tests__/integration.spec.tsx
@@ -0,0 +1,665 @@
+import type { NodeOutPutVar } from '../../../types'
+import type { Condition, LoopNodeType, LoopVariable } from '../types'
+import type { PanelProps } from '@/types/workflow'
+import { fireEvent, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { ErrorHandleMode, ValueType } from '@/app/components/workflow/types'
+import {
+ BlockEnum,
+ VarType,
+} from '../../../types'
+import { VarType as NumberVarType } from '../../tool/types'
+import AddBlock from '../add-block'
+import ConditionAdd from '../components/condition-add'
+import ConditionFilesListValue from '../components/condition-files-list-value'
+import ConditionList from '../components/condition-list'
+import ConditionItem from '../components/condition-list/condition-item'
+import ConditionOperator from '../components/condition-list/condition-operator'
+import ConditionNumberInput from '../components/condition-number-input'
+import ConditionValue from '../components/condition-value'
+import LoopVariables from '../components/loop-variables'
+import FormItem from '../components/loop-variables/form-item'
+import InputModeSelect from '../components/loop-variables/input-mode-selec'
+import VariableTypeSelect from '../components/loop-variables/variable-type-select'
+import InsertBlock from '../insert-block'
+import Node from '../node'
+import Panel from '../panel'
+import {
+ ComparisonOperator,
+ LogicalOperator,
+} from '../types'
+import useConfig from '../use-config'
+
+const mockHandleNodeAdd = vi.fn()
+const mockHandleNodeLoopRerender = vi.fn()
+const mockToastNotify = vi.fn()
+
+vi.mock('reactflow', async () => {
+ const actual = await vi.importActual('reactflow')
+ return {
+ ...actual,
+ Background: ({ id }: { id: string }) => ,
+ useViewport: () => ({ zoom: 1 }),
+ useNodesInitialized: () => true,
+ useStore: (selector: (state: { d3Selection: null, d3Zoom: null }) => unknown) => selector({
+ d3Selection: null,
+ d3Zoom: null,
+ }),
+ }
+})
+
+vi.mock('@/app/components/workflow/block-selector', () => ({
+ __esModule: true,
+ default: ({
+ onSelect,
+ onOpenChange,
+ open,
+ availableBlocksTypes = [],
+ trigger,
+ disabled,
+ }: {
+ onSelect?: (type: BlockEnum) => void
+ onOpenChange?: (open: boolean) => void
+ open?: boolean
+ availableBlocksTypes?: BlockEnum[]
+ trigger?: (open: boolean) => React.ReactNode
+ disabled?: boolean
+ }) => (
+
+ {trigger ?
{trigger(Boolean(open))}
: null}
+
+
+ ),
+}))
+
+vi.mock('../../loop-start', () => ({
+ LoopStartNodeDumb: () => loop-start-node
,
+}))
+
+vi.mock('../use-interactions', () => ({
+ useNodeLoopInteractions: () => ({
+ handleNodeLoopRerender: mockHandleNodeLoopRerender,
+ }),
+}))
+
+vi.mock('../../../hooks', () => ({
+ useAvailableBlocks: () => ({
+ availablePrevBlocks: [],
+ availableNextBlocks: [BlockEnum.LLM],
+ }),
+ useNodesInteractions: () => ({
+ handleNodeAdd: mockHandleNodeAdd,
+ }),
+ useNodesReadOnly: () => ({
+ nodesReadOnly: false,
+ }),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-vars', () => ({
+ __esModule: true,
+ default: ({ onChange }: { onChange: (valueSelector: string[], varItem: { type: VarType }) => void }) => (
+
+ ),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
+ __esModule: true,
+ default: ({ onChange }: { onChange: (value: string) => void }) => (
+
+ ),
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable/variable-label', () => ({
+ VariableLabelInNode: ({ variables }: { variables: string[] }) => {variables.join('.')}
,
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/variable-tag', () => ({
+ __esModule: true,
+ default: ({ valueSelector }: { valueSelector: string[] }) => {valueSelector.join('.')}
,
+}))
+
+const mockWorkflowStoreState = {
+ controlPromptEditorRerenderKey: 0,
+ pipelineId: undefined as string | undefined,
+ setShowInputFieldPanel: vi.fn(),
+}
+
+vi.mock('@/app/components/workflow/store', () => ({
+ useStore: (selector: (state: typeof mockWorkflowStoreState) => unknown) => selector(mockWorkflowStoreState),
+ useWorkflowStore: () => ({
+ getState: () => ({
+ ...mockWorkflowStoreState,
+ conversationVariables: [],
+ dataSourceList: [],
+ setControlPromptEditorRerenderKey: vi.fn(),
+ }),
+ }),
+}))
+
+vi.mock('../../variable-assigner/hooks', () => ({
+ useGetAvailableVars: () => () => [
+ {
+ nodeId: 'node-1',
+ title: 'Start Node',
+ vars: [
+ {
+ variable: 'score',
+ type: VarType.number,
+ },
+ ],
+ },
+ ],
+}))
+
+vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
+ __esModule: true,
+ default: ({ value, onChange }: { value: string, onChange: (value: string) => void }) => (
+