From 4cdffe0bbb9373deefdfce60b3b4c43dd8b8a759 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 24 Jun 2026 17:54:10 +0800 Subject: [PATCH] feat(web): restore snippet selection hook tests --- ...use-create-snippet-from-selection.spec.tsx | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx diff --git a/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx b/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx new file mode 100644 index 00000000000..ddf48659807 --- /dev/null +++ b/web/app/components/snippets/hooks/__tests__/use-create-snippet-from-selection.spec.tsx @@ -0,0 +1,275 @@ +import type { ReactElement } from 'react' +import type { Edge, Node } from '@/app/components/workflow/types' +import type { SnippetCanvasData, SnippetInputField } from '@/models/snippet' +import { act, renderHook } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import { PipelineInputVarType } from '@/models/pipeline' +import { useCreateSnippetFromSelection } from '../use-create-snippet-from-selection' + +const SNIPPET_INPUT_FIELD_NODE_ID = 'start' +const mockHandleOpenCreateSnippetDialog = vi.fn() +const mockHandleCloseCreateSnippetDialog = vi.fn() +const mockHandleCreateSnippet = vi.fn() + +vi.mock('../use-create-snippet', () => ({ + useCreateSnippet: () => ({ + createSnippetMutation: { + isPending: false, + }, + handleCloseCreateSnippetDialog: mockHandleCloseCreateSnippetDialog, + handleCreateSnippet: mockHandleCreateSnippet, + handleOpenCreateSnippetDialog: mockHandleOpenCreateSnippetDialog, + isCreateSnippetDialogOpen: true, + isCreatingSnippet: false, + }), +})) + +type DialogProps = { + selectedGraph?: SnippetCanvasData + inputFields?: SnippetInputField[] +} + +const createNode = ( + id: string, + data: Record, +): Node => ({ + id, + type: 'custom', + position: { x: 0, y: 0 }, + width: 200, + height: 100, + data, +} as unknown as Node) + +describe('useCreateSnippetFromSelection', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should convert environment, conversation, and system variables into snippet input fields', () => { + const selectedNodes = [ + createNode('llm', { + type: BlockEnum.LLM, + prompt: [ + '{{#env.API_KEY#}}', + '{{#conversation.user_name#}}', + '{{#sys.user_id#}}', + '{{#rag.query#}}', + '{{#source.result#}}', + ].join(' '), + model_selector: ['env', 'MODEL_NAME'], + }), + ] + const edges: Edge[] = [] + const onClose = vi.fn() + + const { result } = renderHook(() => useCreateSnippetFromSelection({ + edges, + selectedNodes, + onClose, + })) + + act(() => { + result.current.handleOpenCreateSnippet() + }) + + const dialogProps = (result.current.createSnippetDialog as ReactElement).props + + expect(dialogProps.inputFields).toEqual([ + { + label: 'API_KEY', + variable: 'API_KEY', + type: PipelineInputVarType.textInput, + required: true, + }, + { + label: 'user_name', + variable: 'user_name', + type: PipelineInputVarType.textInput, + required: true, + }, + { + label: 'user_id', + variable: 'user_id', + type: PipelineInputVarType.textInput, + required: true, + }, + { + label: 'result', + variable: 'result', + type: PipelineInputVarType.textInput, + required: true, + }, + { + label: 'MODEL_NAME', + variable: 'MODEL_NAME', + type: PipelineInputVarType.textInput, + required: true, + }, + ]) + const nodeData = dialogProps.selectedGraph?.nodes[0]?.data as Record | undefined + + expect(nodeData?.prompt).toBe([ + `{{#${SNIPPET_INPUT_FIELD_NODE_ID}.API_KEY#}}`, + `{{#${SNIPPET_INPUT_FIELD_NODE_ID}.user_name#}}`, + `{{#${SNIPPET_INPUT_FIELD_NODE_ID}.user_id#}}`, + '{{#rag.query#}}', + `{{#${SNIPPET_INPUT_FIELD_NODE_ID}.result#}}`, + ].join(' ')) + expect(nodeData?.model_selector).toEqual([ + SNIPPET_INPUT_FIELD_NODE_ID, + 'MODEL_NAME', + ]) + expect(onClose).toHaveBeenCalled() + }) + + it('should convert system variables used by if-else and variable aggregator nodes', () => { + const selectedNodes = [ + createNode('llm', { + type: BlockEnum.LLM, + title: 'LLM', + }), + createNode('if-else', { + type: BlockEnum.IfElse, + cases: [{ + case_id: 'case-1', + conditions: [{ + id: 'condition-1', + variable_selector: ['sys', 'query'], + comparison_operator: 'contains', + value: 'hello', + }], + }], + }), + createNode('variable-aggregator', { + type: BlockEnum.VariableAggregator, + variables: [ + ['sys', 'files'], + ['llm', 'text'], + ], + advanced_settings: { + group_enabled: true, + groups: [{ + groupId: 'group-1', + group_name: 'Group1', + variables: [ + ['sys', 'workflow_id'], + ], + }], + }, + }), + ] + const onClose = vi.fn() + + const { result } = renderHook(() => useCreateSnippetFromSelection({ + edges: [], + selectedNodes, + onClose, + })) + + act(() => { + result.current.handleOpenCreateSnippet() + }) + + const dialogProps = (result.current.createSnippetDialog as ReactElement).props + + expect(dialogProps.inputFields).toEqual([ + { + label: 'query', + variable: 'query', + type: PipelineInputVarType.textInput, + required: true, + }, + { + label: 'files', + variable: 'files', + type: PipelineInputVarType.multiFiles, + required: true, + }, + { + label: 'workflow_id', + variable: 'workflow_id', + type: PipelineInputVarType.textInput, + required: true, + }, + ]) + + const ifElseNode = dialogProps.selectedGraph?.nodes.find(node => node.id === 'if-else') + const aggregatorNode = dialogProps.selectedGraph?.nodes.find(node => node.id === 'variable-aggregator') + const ifElseData = ifElseNode?.data as Record + const aggregatorData = aggregatorNode?.data as { + variables?: string[][] + advanced_settings?: { groups?: Array<{ variables?: string[][] }> } + } + + expect(ifElseData.cases).toEqual([{ + case_id: 'case-1', + conditions: [{ + id: 'condition-1', + variable_selector: [SNIPPET_INPUT_FIELD_NODE_ID, 'query'], + comparison_operator: 'contains', + value: 'hello', + }], + }]) + expect(aggregatorData.variables).toEqual([ + [SNIPPET_INPUT_FIELD_NODE_ID, 'files'], + ['llm', 'text'], + ]) + expect(aggregatorData.advanced_settings?.groups?.[0]?.variables).toEqual([ + [SNIPPET_INPUT_FIELD_NODE_ID, 'workflow_id'], + ]) + }) + + it('should keep #context# prompt placeholders when creating a snippet from workflow selection', () => { + const selectedNodes = [ + createNode('llm', { + type: BlockEnum.LLM, + context: { + enabled: true, + variable_selector: ['code', 'result'], + }, + prompt: '{{#context#}} {{#code.summary#}}', + }), + ] + const onClose = vi.fn() + + const { result } = renderHook(() => useCreateSnippetFromSelection({ + edges: [], + selectedNodes, + onClose, + })) + + act(() => { + result.current.handleOpenCreateSnippet() + }) + + const dialogProps = (result.current.createSnippetDialog as ReactElement).props + const nodeData = dialogProps.selectedGraph?.nodes[0]?.data as { + context?: { + enabled: boolean + variable_selector: string[] + } + prompt?: string + } + + expect(dialogProps.inputFields).toEqual([ + { + label: 'result', + variable: 'result', + type: PipelineInputVarType.textInput, + required: true, + }, + { + label: 'summary', + variable: 'summary', + type: PipelineInputVarType.textInput, + required: true, + }, + ]) + expect(nodeData.context).toEqual({ + enabled: true, + variable_selector: [SNIPPET_INPUT_FIELD_NODE_ID, 'result'], + }) + expect(nodeData.prompt).toBe(`{{#context#}} {{#${SNIPPET_INPUT_FIELD_NODE_ID}.summary#}}`) + }) +})