mirror of
https://github.com/langgenius/dify.git
synced 2026-04-24 00:59:19 +08:00
507 lines
14 KiB
TypeScript
507 lines
14 KiB
TypeScript
import type { RAGPipelineVariables, VAR_TYPE_MAP } from '@/models/pipeline'
|
|
import { renderHook } from '@testing-library/react'
|
|
import { act } from 'react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { BlockEnum } from '@/app/components/workflow/types'
|
|
import { Resolution, TransferMethod } from '@/types/app'
|
|
import { FlowType } from '@/types/common'
|
|
|
|
import {
|
|
useAvailableNodesMetaData,
|
|
useDSL,
|
|
useGetRunAndTraceUrl,
|
|
useInputFieldPanel,
|
|
useNodesSyncDraft,
|
|
usePipelineInit,
|
|
usePipelineRefreshDraft,
|
|
usePipelineRun,
|
|
usePipelineStartRun,
|
|
} from '../index'
|
|
import { useConfigsMap } from '../use-configs-map'
|
|
import { useConfigurations, useInitialData } from '../use-input-fields'
|
|
import { usePipelineTemplate } from '../use-pipeline-template'
|
|
|
|
const _mockGetState = vi.fn()
|
|
const mockUseStore = vi.fn()
|
|
const mockUseWorkflowStore = vi.fn()
|
|
|
|
vi.mock('@/app/components/workflow/store', () => ({
|
|
useStore: (selector: (state: Record<string, unknown>) => unknown) => mockUseStore(selector),
|
|
useWorkflowStore: () => mockUseWorkflowStore(),
|
|
}))
|
|
|
|
const mockNotify = vi.fn()
|
|
vi.mock('@/app/components/base/toast/context', () => ({
|
|
useToastContext: () => ({
|
|
notify: mockNotify,
|
|
}),
|
|
}))
|
|
|
|
const mockEventEmit = vi.fn()
|
|
vi.mock('@/context/event-emitter', () => ({
|
|
useEventEmitterContextContext: () => ({
|
|
eventEmitter: {
|
|
emit: mockEventEmit,
|
|
},
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/context/i18n', () => ({
|
|
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/constants', () => ({
|
|
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
|
WORKFLOW_DATA_UPDATE: 'WORKFLOW_DATA_UPDATE',
|
|
START_INITIAL_POSITION: { x: 100, y: 100 },
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/constants/node', () => ({
|
|
WORKFLOW_COMMON_NODES: [
|
|
{
|
|
metaData: { type: BlockEnum.Start },
|
|
defaultValue: { type: BlockEnum.Start },
|
|
},
|
|
{
|
|
metaData: { type: BlockEnum.End },
|
|
defaultValue: { type: BlockEnum.End },
|
|
},
|
|
],
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/data-source-empty/default', () => ({
|
|
default: {
|
|
metaData: { type: BlockEnum.DataSourceEmpty },
|
|
defaultValue: { type: BlockEnum.DataSourceEmpty },
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/data-source/default', () => ({
|
|
default: {
|
|
metaData: { type: BlockEnum.DataSource },
|
|
defaultValue: { type: BlockEnum.DataSource },
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/nodes/knowledge-base/default', () => ({
|
|
default: {
|
|
metaData: { type: BlockEnum.KnowledgeBase },
|
|
defaultValue: { type: BlockEnum.KnowledgeBase },
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
|
const actual = await importOriginal() as Record<string, unknown>
|
|
return {
|
|
...actual,
|
|
generateNewNode: ({ id, data, position }: { id: string, data: object, position: { x: number, y: number } }) => ({
|
|
newNode: { id, data, position, type: 'custom' },
|
|
}),
|
|
}
|
|
})
|
|
|
|
const mockExportPipelineConfig = vi.fn()
|
|
vi.mock('@/service/use-pipeline', () => ({
|
|
useExportPipelineDSL: () => ({
|
|
mutateAsync: mockExportPipelineConfig,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/service/workflow', () => ({
|
|
fetchWorkflowDraft: vi.fn().mockResolvedValue({
|
|
graph: { nodes: [], edges: [], viewport: {} },
|
|
environment_variables: [],
|
|
}),
|
|
}))
|
|
|
|
describe('useConfigsMap', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUseStore.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => {
|
|
const state = {
|
|
pipelineId: 'test-pipeline-id',
|
|
fileUploadConfig: { max_file_size: 10 },
|
|
}
|
|
return selector(state)
|
|
})
|
|
})
|
|
|
|
it('should return config map with correct flowId', () => {
|
|
const { result } = renderHook(() => useConfigsMap())
|
|
|
|
expect(result.current.flowId).toBe('test-pipeline-id')
|
|
})
|
|
|
|
it('should return config map with correct flowType', () => {
|
|
const { result } = renderHook(() => useConfigsMap())
|
|
|
|
expect(result.current.flowType).toBe(FlowType.ragPipeline)
|
|
})
|
|
|
|
it('should return file settings with image config', () => {
|
|
const { result } = renderHook(() => useConfigsMap())
|
|
|
|
expect(result.current.fileSettings.image).toEqual({
|
|
enabled: false,
|
|
detail: Resolution.high,
|
|
number_limits: 3,
|
|
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
|
})
|
|
})
|
|
|
|
it('should include fileUploadConfig from store', () => {
|
|
const { result } = renderHook(() => useConfigsMap())
|
|
|
|
expect(result.current.fileSettings.fileUploadConfig).toEqual({ max_file_size: 10 })
|
|
})
|
|
})
|
|
|
|
describe('useGetRunAndTraceUrl', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUseWorkflowStore.mockReturnValue({
|
|
getState: () => ({
|
|
pipelineId: 'pipeline-123',
|
|
}),
|
|
})
|
|
})
|
|
|
|
it('should return getWorkflowRunAndTraceUrl function', () => {
|
|
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
|
|
|
expect(result.current.getWorkflowRunAndTraceUrl).toBeDefined()
|
|
expect(typeof result.current.getWorkflowRunAndTraceUrl).toBe('function')
|
|
})
|
|
|
|
it('should generate correct run URL', () => {
|
|
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
|
|
|
const { runUrl } = result.current.getWorkflowRunAndTraceUrl('run-456')
|
|
|
|
expect(runUrl).toBe('/rag/pipelines/pipeline-123/workflow-runs/run-456')
|
|
})
|
|
|
|
it('should generate correct trace URL', () => {
|
|
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
|
|
|
const { traceUrl } = result.current.getWorkflowRunAndTraceUrl('run-456')
|
|
|
|
expect(traceUrl).toBe('/rag/pipelines/pipeline-123/workflow-runs/run-456/node-executions')
|
|
})
|
|
})
|
|
|
|
describe('useInputFieldPanel', () => {
|
|
const mockSetShowInputFieldPanel = vi.fn()
|
|
const mockSetShowInputFieldPreviewPanel = vi.fn()
|
|
const mockSetInputFieldEditPanelProps = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockUseStore.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => {
|
|
const state = {
|
|
showInputFieldPreviewPanel: false,
|
|
inputFieldEditPanelProps: null,
|
|
}
|
|
return selector(state)
|
|
})
|
|
mockUseWorkflowStore.mockReturnValue({
|
|
getState: () => ({
|
|
showInputFieldPreviewPanel: false,
|
|
setShowInputFieldPanel: mockSetShowInputFieldPanel,
|
|
setShowInputFieldPreviewPanel: mockSetShowInputFieldPreviewPanel,
|
|
setInputFieldEditPanelProps: mockSetInputFieldEditPanelProps,
|
|
}),
|
|
})
|
|
})
|
|
|
|
it('should return isPreviewing as false when showInputFieldPreviewPanel is false', () => {
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
|
|
expect(result.current.isPreviewing).toBe(false)
|
|
})
|
|
|
|
it('should return isPreviewing as true when showInputFieldPreviewPanel is true', () => {
|
|
mockUseStore.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => {
|
|
const state = {
|
|
showInputFieldPreviewPanel: true,
|
|
inputFieldEditPanelProps: null,
|
|
}
|
|
return selector(state)
|
|
})
|
|
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
|
|
expect(result.current.isPreviewing).toBe(true)
|
|
})
|
|
|
|
it('should return isEditing as false when inputFieldEditPanelProps is null', () => {
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
|
|
expect(result.current.isEditing).toBe(false)
|
|
})
|
|
|
|
it('should return isEditing as true when inputFieldEditPanelProps exists', () => {
|
|
mockUseStore.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => {
|
|
const state = {
|
|
showInputFieldPreviewPanel: false,
|
|
inputFieldEditPanelProps: { some: 'props' },
|
|
}
|
|
return selector(state)
|
|
})
|
|
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
|
|
expect(result.current.isEditing).toBe(true)
|
|
})
|
|
|
|
it('should call all setters when closeAllInputFieldPanels is called', () => {
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
|
|
act(() => {
|
|
result.current.closeAllInputFieldPanels()
|
|
})
|
|
|
|
expect(mockSetShowInputFieldPanel).toHaveBeenCalledWith(false)
|
|
expect(mockSetShowInputFieldPreviewPanel).toHaveBeenCalledWith(false)
|
|
expect(mockSetInputFieldEditPanelProps).toHaveBeenCalledWith(null)
|
|
})
|
|
|
|
it('should toggle preview panel when toggleInputFieldPreviewPanel is called', () => {
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
|
|
act(() => {
|
|
result.current.toggleInputFieldPreviewPanel()
|
|
})
|
|
|
|
expect(mockSetShowInputFieldPreviewPanel).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
it('should set edit panel props when toggleInputFieldEditPanel is called', () => {
|
|
const { result } = renderHook(() => useInputFieldPanel())
|
|
const editContent = { onClose: vi.fn(), onSubmit: vi.fn() }
|
|
|
|
act(() => {
|
|
result.current.toggleInputFieldEditPanel(editContent)
|
|
})
|
|
|
|
expect(mockSetInputFieldEditPanelProps).toHaveBeenCalledWith(editContent)
|
|
})
|
|
})
|
|
|
|
describe('useInitialData', () => {
|
|
it('should return empty object for empty variables', () => {
|
|
const { result } = renderHook(() => useInitialData([], undefined))
|
|
|
|
expect(result.current).toEqual({})
|
|
})
|
|
|
|
it('should handle text input type with default value', () => {
|
|
const variables: RAGPipelineVariables = [
|
|
{
|
|
type: 'text-input' as keyof typeof VAR_TYPE_MAP,
|
|
variable: 'textVar',
|
|
label: 'Text',
|
|
required: false,
|
|
default_value: 'default text',
|
|
belong_to_node_id: 'node-1',
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() => useInitialData(variables, undefined))
|
|
|
|
expect(result.current.textVar).toBe('default text')
|
|
})
|
|
|
|
it('should use lastRunInputData over default value', () => {
|
|
const variables: RAGPipelineVariables = [
|
|
{
|
|
type: 'text-input' as keyof typeof VAR_TYPE_MAP,
|
|
variable: 'textVar',
|
|
label: 'Text',
|
|
required: false,
|
|
default_value: 'default text',
|
|
belong_to_node_id: 'node-1',
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() => useInitialData(variables, { textVar: 'last run value' }))
|
|
|
|
expect(result.current.textVar).toBe('last run value')
|
|
})
|
|
|
|
it('should handle number input type with default 0', () => {
|
|
const variables: RAGPipelineVariables = [
|
|
{
|
|
type: 'number' as keyof typeof VAR_TYPE_MAP,
|
|
variable: 'numVar',
|
|
label: 'Number',
|
|
required: false,
|
|
belong_to_node_id: 'node-1',
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() => useInitialData(variables, undefined))
|
|
|
|
expect(result.current.numVar).toBe(0)
|
|
})
|
|
|
|
it('should handle file type with default empty array', () => {
|
|
const variables: RAGPipelineVariables = [
|
|
{
|
|
type: 'file' as keyof typeof VAR_TYPE_MAP,
|
|
variable: 'fileVar',
|
|
label: 'File',
|
|
required: false,
|
|
belong_to_node_id: 'node-1',
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() => useInitialData(variables, undefined))
|
|
|
|
expect(result.current.fileVar).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('useConfigurations', () => {
|
|
it('should return empty array for empty variables', () => {
|
|
const { result } = renderHook(() => useConfigurations([]))
|
|
|
|
expect(result.current).toEqual([])
|
|
})
|
|
|
|
it('should transform variables to configurations', () => {
|
|
const variables: RAGPipelineVariables = [
|
|
{
|
|
type: 'text-input' as keyof typeof VAR_TYPE_MAP,
|
|
variable: 'textVar',
|
|
label: 'Text Label',
|
|
required: true,
|
|
max_length: 100,
|
|
placeholder: 'Enter text',
|
|
tooltips: 'Help text',
|
|
belong_to_node_id: 'node-1',
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() => useConfigurations(variables))
|
|
|
|
expect(result.current.length).toBe(1)
|
|
expect(result.current[0].variable).toBe('textVar')
|
|
expect(result.current[0].label).toBe('Text Label')
|
|
expect(result.current[0].required).toBe(true)
|
|
expect(result.current[0].maxLength).toBe(100)
|
|
expect(result.current[0].placeholder).toBe('Enter text')
|
|
expect(result.current[0].tooltip).toBe('Help text')
|
|
})
|
|
|
|
it('should transform options correctly', () => {
|
|
const variables: RAGPipelineVariables = [
|
|
{
|
|
type: 'select' as keyof typeof VAR_TYPE_MAP,
|
|
variable: 'selectVar',
|
|
label: 'Select',
|
|
required: false,
|
|
options: ['option1', 'option2', 'option3'],
|
|
belong_to_node_id: 'node-1',
|
|
},
|
|
]
|
|
|
|
const { result } = renderHook(() => useConfigurations(variables))
|
|
|
|
expect(result.current[0].options).toEqual([
|
|
{ label: 'option1', value: 'option1' },
|
|
{ label: 'option2', value: 'option2' },
|
|
{ label: 'option3', value: 'option3' },
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('useAvailableNodesMetaData', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should return nodes array', () => {
|
|
const { result } = renderHook(() => useAvailableNodesMetaData())
|
|
|
|
expect(result.current.nodes).toBeDefined()
|
|
expect(Array.isArray(result.current.nodes)).toBe(true)
|
|
})
|
|
|
|
it('should return nodesMap object', () => {
|
|
const { result } = renderHook(() => useAvailableNodesMetaData())
|
|
|
|
expect(result.current.nodesMap).toBeDefined()
|
|
expect(typeof result.current.nodesMap).toBe('object')
|
|
})
|
|
})
|
|
|
|
describe('usePipelineTemplate', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should return nodes array with knowledge base node', () => {
|
|
const { result } = renderHook(() => usePipelineTemplate())
|
|
|
|
expect(result.current.nodes).toBeDefined()
|
|
expect(Array.isArray(result.current.nodes)).toBe(true)
|
|
expect(result.current.nodes.length).toBe(1)
|
|
})
|
|
|
|
it('should return empty edges array', () => {
|
|
const { result } = renderHook(() => usePipelineTemplate())
|
|
|
|
expect(result.current.edges).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('useDSL', () => {
|
|
it('should be defined and exported', () => {
|
|
expect(useDSL).toBeDefined()
|
|
expect(typeof useDSL).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe('exports', () => {
|
|
it('should export useAvailableNodesMetaData', () => {
|
|
expect(useAvailableNodesMetaData).toBeDefined()
|
|
})
|
|
|
|
it('should export useDSL', () => {
|
|
expect(useDSL).toBeDefined()
|
|
})
|
|
|
|
it('should export useGetRunAndTraceUrl', () => {
|
|
expect(useGetRunAndTraceUrl).toBeDefined()
|
|
})
|
|
|
|
it('should export useInputFieldPanel', () => {
|
|
expect(useInputFieldPanel).toBeDefined()
|
|
})
|
|
|
|
it('should export useNodesSyncDraft', () => {
|
|
expect(useNodesSyncDraft).toBeDefined()
|
|
})
|
|
|
|
it('should export usePipelineInit', () => {
|
|
expect(usePipelineInit).toBeDefined()
|
|
})
|
|
|
|
it('should export usePipelineRefreshDraft', () => {
|
|
expect(usePipelineRefreshDraft).toBeDefined()
|
|
})
|
|
|
|
it('should export usePipelineRun', () => {
|
|
expect(usePipelineRun).toBeDefined()
|
|
})
|
|
|
|
it('should export usePipelineStartRun', () => {
|
|
expect(usePipelineStartRun).toBeDefined()
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|