diff --git a/web/app/components/workflow/nodes/list-operator/components/__tests__/filter-condition.spec.tsx b/web/app/components/workflow/nodes/list-operator/components/__tests__/filter-condition.spec.tsx index 38034632c6a..870be62f560 100644 --- a/web/app/components/workflow/nodes/list-operator/components/__tests__/filter-condition.spec.tsx +++ b/web/app/components/workflow/nodes/list-operator/components/__tests__/filter-condition.spec.tsx @@ -1,64 +1,85 @@ -import { fireEvent, render, screen } from '@testing-library/react' +import type { Condition } from '../../types' +import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { replacePromptEditorText } from '@/app/components/base/prompt-editor/plugins/__tests__/test-helpers' +import { + createEdge, + createNode, + createStartNode, +} from '@/app/components/workflow/__tests__/fixtures' +import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' +import { consoleQuery } from '@/service/console' +import { seedModelProviderQueries } from '@/test/console/model-provider' +import { createConsoleQueryWrapper } from '@/test/console/query-data' +import { seedToolProviders } from '@/test/console/tool-providers' import { TransferMethod } from '@/types/app' -import { VarType } from '../../../../types' +import { BlockEnum, InputVarType, VarType } from '../../../../types' import { ComparisonOperator } from '../../../if-else/types' import FilterCondition from '../filter-condition' -const { mockUseAvailableVarList } = vi.hoisted(() => ({ - mockUseAvailableVarList: vi.fn((_nodeId: string, _options: unknown) => ({ - availableVars: [], - availableNodesWithParent: [], - })), -})) +const renderCondition = (ui: React.ReactElement) => { + const { queryClient } = createConsoleQueryWrapper({ appDslVersion: '0.6.0' }) + seedModelProviderQueries(queryClient, []) + seedToolProviders(queryClient, { + builtInTools: [], + customTools: [], + workflowTools: [], + mcpTools: [], + }) + queryClient.setQueryData(consoleQuery.spec.schemaDefinitions.get.queryKey(), []) -vi.mock('@/app/components/workflow/nodes/_base/hooks/use-available-var-list', () => ({ - default: (nodeId: string, options: unknown) => mockUseAvailableVarList(nodeId, options), -})) - -vi.mock('@/app/components/workflow/nodes/_base/components/input-support-select-var', () => ({ - default: ({ - value, - onChange, - onFocusChange, - readOnly, - placeholder, - className, - }: { - value: string - onChange: (value: string) => void - onFocusChange?: (value: boolean) => void - readOnly?: boolean - placeholder?: string - className?: string - }) => ( - onChange(e.target.value)} - onFocus={() => onFocusChange?.(true)} - onBlur={() => onFocusChange?.(false)} - readOnly={readOnly} - placeholder={placeholder} - /> - ), -})) - -describe('FilterCondition', () => { - beforeEach(() => { - vi.clearAllMocks() - mockUseAvailableVarList.mockReturnValue({ - availableVars: [], - availableNodesWithParent: [], - }) + const start = createStartNode({ + id: 'start', + data: { + title: 'File input', + variables: [ + { + variable: 'file_name', + label: 'File name', + type: InputVarType.textInput, + required: true, + }, + ], + }, + }) + const code = createNode({ + id: 'code', + data: { + title: 'File stats', + type: BlockEnum.Code, + outputs: { file_size: { type: VarType.number, children: null } }, + }, + }) + const current = createNode({ + id: 'node-1', + data: { title: 'List operator', type: BlockEnum.ListFilter }, }) + return renderWorkflowFlowComponent(ui, { + queryClient, + nodes: [start, code, current], + edges: [ + createEdge({ + source: start.id, + target: code.id, + data: { sourceType: BlockEnum.Start, targetType: BlockEnum.Code }, + }), + createEdge({ + source: code.id, + target: current.id, + data: { sourceType: BlockEnum.Code, targetType: BlockEnum.ListFilter }, + }), + ], + }) +} + +describe('FilterCondition', () => { it('should render a select input for array-backed file conditions and update array values', async () => { const user = userEvent.setup() const onChange = vi.fn() - render( + renderCondition( { const user = userEvent.setup() const onChange = vi.fn() - render( + renderCondition( { it('should update a file name condition and accept only string variable references', async () => { const user = userEvent.setup() - const onChange = vi.fn() - - render( - , - ) - - const variableInput = screen.getByRole('textbox', { name: 'variable-input' }) - expect(variableInput).toHaveAttribute( - 'placeholder', - 'workflowIntegrations.nodes.http.insertVarPlaceholder', - ) - - await user.click(variableInput) - - fireEvent.change(variableInput, { target: { value: 'draft next' } }) - expect(onChange).toHaveBeenLastCalledWith({ + const initialCondition: Condition = { key: 'name', comparison_operator: ComparisonOperator.equal, - value: 'draft next', - }) - - const config = mockUseAvailableVarList.mock.calls[0]?.[1] as { - filterVar: (varPayload: { type: VarType }) => boolean + value: 'draft', } - expect(config.filterVar({ type: VarType.string })).toBe(true) - expect(config.filterVar({ type: VarType.number })).toBe(false) + const ControlledCondition = () => { + const [condition, setCondition] = useState(initialCondition) + return ( + <> + + {JSON.stringify(condition)} + + ) + } + + renderCondition() + + const editor = screen.getByRole('textbox') + await waitFor(() => expect(editor).toHaveTextContent('draft')) + await replacePromptEditorText(editor, 'draft next ') + await user.click(editor) + await user.type(editor, '/') + + expect(await screen.findByText('file_name')).toBeVisible() + expect(screen.queryByText('file_size')).not.toBeInTheDocument() + await user.click(screen.getByText('file_name')) + + await waitFor(() => + expect( + JSON.parse(screen.getByRole('status', { name: 'Saved condition' }).textContent!), + ).toEqual({ + key: 'name', + comparison_operator: ComparisonOperator.equal, + value: 'draft next {{#start.file_name#}}', + }), + ) }) it('should reset operator and value when the sub variable changes', async () => { const user = userEvent.setup() const onChange = vi.fn() - render( + renderCondition( { it('should render fallback inputs for unsupported keys and hide value inputs for no-value operators', async () => { const onChange = vi.fn() - const { rerender } = render( + const { rerender } = renderCondition( { />, ) - expect(screen.queryByRole('textbox', { name: 'variable-input' })).not.toBeInTheDocument() + expect(screen.queryByRole('textbox')).not.toBeInTheDocument() expect(screen.queryByRole('spinbutton')).not.toBeInTheDocument() }) @@ -237,7 +264,7 @@ describe('FilterCondition', () => { const user = userEvent.setup() const onChange = vi.fn() - const { rerender } = render( + const { rerender } = renderCondition( { it('prevents boolean and sub-variable changes in read-only mode', async () => { const user = userEvent.setup() const onChange = vi.fn() - const { rerender } = render( + const { rerender } = renderCondition( { }) expect(subVariable).toBeDisabled() await user.click(subVariable) - expect(screen.queryByRole('listbox')).not.toBeInTheDocument() + expect(screen.queryByRole('option', { name: 'name' })).not.toBeInTheDocument() expect(onChange).not.toHaveBeenCalled() }) })