mirror of
https://github.com/langgenius/dify.git
synced 2026-03-31 02:48:49 +08:00
test: add integration tests for DSL export/import flow, input field CRUD, and test run flow
This commit is contained in:
179
web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts
Normal file
179
web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts
Normal file
@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Integration test: DSL export/import flow
|
||||
*
|
||||
* Validates DSL export logic (sync draft → check secrets → download)
|
||||
* and DSL import modal state management.
|
||||
*/
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockDoSyncWorkflowDraft = vi.fn().mockResolvedValue(undefined)
|
||||
const mockExportPipelineConfig = vi.fn().mockResolvedValue({ data: 'yaml-content' })
|
||||
const mockNotify = vi.fn()
|
||||
const mockEventEmitter = { emit: vi.fn() }
|
||||
const mockDownloadBlob = vi.fn()
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
pipelineId: 'pipeline-abc',
|
||||
knowledgeName: 'My Pipeline',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: mockEventEmitter,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useExportPipelineDSL: () => ({
|
||||
mutateAsync: mockExportPipelineConfig,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/rag-pipeline/hooks/use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
doSyncWorkflowDraft: mockDoSyncWorkflowDraft,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('DSL Export/Import Flow', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Export Flow', () => {
|
||||
it('should sync draft then export then download', async () => {
|
||||
const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
|
||||
expect(mockExportPipelineConfig).toHaveBeenCalledWith({
|
||||
pipelineId: 'pipeline-abc',
|
||||
include: false,
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({
|
||||
fileName: 'My Pipeline.pipeline',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should export with include flag when specified', async () => {
|
||||
const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL(true)
|
||||
})
|
||||
|
||||
expect(mockExportPipelineConfig).toHaveBeenCalledWith({
|
||||
pipelineId: 'pipeline-abc',
|
||||
include: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should notify on export error', async () => {
|
||||
mockDoSyncWorkflowDraft.mockRejectedValueOnce(new Error('sync failed'))
|
||||
const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Export Check Flow', () => {
|
||||
it('should export directly when no secret environment variables', async () => {
|
||||
const { fetchWorkflowDraft } = await import('@/service/workflow')
|
||||
vi.mocked(fetchWorkflowDraft).mockResolvedValueOnce({
|
||||
environment_variables: [
|
||||
{ value_type: 'string', key: 'API_URL', value: 'https://api.example.com' },
|
||||
],
|
||||
} as unknown as Awaited<ReturnType<typeof fetchWorkflowDraft>>)
|
||||
|
||||
const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
})
|
||||
|
||||
// Should proceed to export directly (no secret vars)
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should emit DSL_EXPORT_CHECK event when secret variables exist', async () => {
|
||||
const { fetchWorkflowDraft } = await import('@/service/workflow')
|
||||
vi.mocked(fetchWorkflowDraft).mockResolvedValueOnce({
|
||||
environment_variables: [
|
||||
{ value_type: 'secret', key: 'API_KEY', value: '***' },
|
||||
],
|
||||
} as unknown as Awaited<ReturnType<typeof fetchWorkflowDraft>>)
|
||||
|
||||
const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
})
|
||||
|
||||
expect(mockEventEmitter.emit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'DSL_EXPORT_CHECK',
|
||||
payload: expect.objectContaining({
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({ value_type: 'secret' }),
|
||||
]),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should notify on export check error', async () => {
|
||||
const { fetchWorkflowDraft } = await import('@/service/workflow')
|
||||
vi.mocked(fetchWorkflowDraft).mockRejectedValueOnce(new Error('fetch failed'))
|
||||
|
||||
const { useDSL } = await import('@/app/components/rag-pipeline/hooks/use-DSL')
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCheck()
|
||||
})
|
||||
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
278
web/__tests__/rag-pipeline/input-field-crud-flow.test.ts
Normal file
278
web/__tests__/rag-pipeline/input-field-crud-flow.test.ts
Normal file
@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Integration test: Input field CRUD complete flow
|
||||
*
|
||||
* Validates the full lifecycle of input fields:
|
||||
* creation, editing, renaming, removal, and data conversion round-trip.
|
||||
*/
|
||||
import type { FormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
VAR_ITEM_TEMPLATE_IN_PIPELINE: {
|
||||
type: 'text-input',
|
||||
label: '',
|
||||
variable: '',
|
||||
max_length: 48,
|
||||
default_value: undefined,
|
||||
required: true,
|
||||
tooltips: undefined,
|
||||
options: [],
|
||||
placeholder: undefined,
|
||||
unit: undefined,
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Input Field CRUD Flow', () => {
|
||||
describe('Create → Edit → Convert Round-trip', () => {
|
||||
it('should create a text field and roundtrip through form data', async () => {
|
||||
const { convertToInputFieldFormData, convertFormDataToINputField } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
// Create new field from template (no data passed)
|
||||
const newFormData = convertToInputFieldFormData()
|
||||
expect(newFormData.type).toBe('text-input')
|
||||
expect(newFormData.variable).toBe('')
|
||||
expect(newFormData.label).toBe('')
|
||||
expect(newFormData.required).toBe(true)
|
||||
|
||||
// Simulate user editing form data
|
||||
const editedFormData: FormData = {
|
||||
...newFormData,
|
||||
variable: 'user_name',
|
||||
label: 'User Name',
|
||||
maxLength: 100,
|
||||
default: 'John',
|
||||
tooltips: 'Enter your name',
|
||||
placeholder: 'Type here...',
|
||||
allowedTypesAndExtensions: {},
|
||||
}
|
||||
|
||||
// Convert back to InputVar
|
||||
const inputVar = convertFormDataToINputField(editedFormData)
|
||||
|
||||
expect(inputVar.variable).toBe('user_name')
|
||||
expect(inputVar.label).toBe('User Name')
|
||||
expect(inputVar.max_length).toBe(100)
|
||||
expect(inputVar.default_value).toBe('John')
|
||||
expect(inputVar.tooltips).toBe('Enter your name')
|
||||
expect(inputVar.placeholder).toBe('Type here...')
|
||||
expect(inputVar.required).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle file field with upload settings', async () => {
|
||||
const { convertToInputFieldFormData, convertFormDataToINputField } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
const fileInputVar: InputVar = {
|
||||
type: PipelineInputVarType.singleFile,
|
||||
label: 'Upload Document',
|
||||
variable: 'doc_file',
|
||||
max_length: 1,
|
||||
default_value: undefined,
|
||||
required: true,
|
||||
tooltips: 'Upload a PDF',
|
||||
options: [],
|
||||
placeholder: undefined,
|
||||
unit: undefined,
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
allowed_file_types: [SupportUploadFileTypes.document],
|
||||
allowed_file_extensions: ['.pdf', '.docx'],
|
||||
}
|
||||
|
||||
// Convert to form data
|
||||
const formData = convertToInputFieldFormData(fileInputVar)
|
||||
expect(formData.allowedFileUploadMethods).toEqual([TransferMethod.local_file, TransferMethod.remote_url])
|
||||
expect(formData.allowedTypesAndExtensions).toEqual({
|
||||
allowedFileTypes: [SupportUploadFileTypes.document],
|
||||
allowedFileExtensions: ['.pdf', '.docx'],
|
||||
})
|
||||
|
||||
// Round-trip back
|
||||
const restored = convertFormDataToINputField(formData)
|
||||
expect(restored.allowed_file_upload_methods).toEqual([TransferMethod.local_file, TransferMethod.remote_url])
|
||||
expect(restored.allowed_file_types).toEqual([SupportUploadFileTypes.document])
|
||||
expect(restored.allowed_file_extensions).toEqual(['.pdf', '.docx'])
|
||||
})
|
||||
|
||||
it('should handle select field with options', async () => {
|
||||
const { convertToInputFieldFormData, convertFormDataToINputField } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
const selectVar: InputVar = {
|
||||
type: PipelineInputVarType.select,
|
||||
label: 'Priority',
|
||||
variable: 'priority',
|
||||
max_length: 0,
|
||||
default_value: 'medium',
|
||||
required: false,
|
||||
tooltips: 'Select priority level',
|
||||
options: ['low', 'medium', 'high'],
|
||||
placeholder: 'Choose...',
|
||||
unit: undefined,
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
}
|
||||
|
||||
const formData = convertToInputFieldFormData(selectVar)
|
||||
expect(formData.options).toEqual(['low', 'medium', 'high'])
|
||||
expect(formData.default).toBe('medium')
|
||||
|
||||
const restored = convertFormDataToINputField(formData)
|
||||
expect(restored.options).toEqual(['low', 'medium', 'high'])
|
||||
expect(restored.default_value).toBe('medium')
|
||||
})
|
||||
|
||||
it('should handle number field with unit', async () => {
|
||||
const { convertToInputFieldFormData, convertFormDataToINputField } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
const numberVar: InputVar = {
|
||||
type: PipelineInputVarType.number,
|
||||
label: 'Max Tokens',
|
||||
variable: 'max_tokens',
|
||||
max_length: 0,
|
||||
default_value: '1024',
|
||||
required: true,
|
||||
tooltips: undefined,
|
||||
options: [],
|
||||
placeholder: undefined,
|
||||
unit: 'tokens',
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
}
|
||||
|
||||
const formData = convertToInputFieldFormData(numberVar)
|
||||
expect(formData.unit).toBe('tokens')
|
||||
expect(formData.default).toBe('1024')
|
||||
|
||||
const restored = convertFormDataToINputField(formData)
|
||||
expect(restored.unit).toBe('tokens')
|
||||
expect(restored.default_value).toBe('1024')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Omit optional fields', () => {
|
||||
it('should not include tooltips when undefined', async () => {
|
||||
const { convertToInputFieldFormData } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
const inputVar: InputVar = {
|
||||
type: PipelineInputVarType.textInput,
|
||||
label: 'Test',
|
||||
variable: 'test',
|
||||
max_length: 48,
|
||||
default_value: undefined,
|
||||
required: true,
|
||||
tooltips: undefined,
|
||||
options: [],
|
||||
placeholder: undefined,
|
||||
unit: undefined,
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
}
|
||||
|
||||
const formData = convertToInputFieldFormData(inputVar)
|
||||
|
||||
// Optional fields should not be present
|
||||
expect('tooltips' in formData).toBe(false)
|
||||
expect('placeholder' in formData).toBe(false)
|
||||
expect('unit' in formData).toBe(false)
|
||||
expect('default' in formData).toBe(false)
|
||||
})
|
||||
|
||||
it('should include optional fields when explicitly set to empty string', async () => {
|
||||
const { convertToInputFieldFormData } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
const inputVar: InputVar = {
|
||||
type: PipelineInputVarType.textInput,
|
||||
label: 'Test',
|
||||
variable: 'test',
|
||||
max_length: 48,
|
||||
default_value: '',
|
||||
required: true,
|
||||
tooltips: '',
|
||||
options: [],
|
||||
placeholder: '',
|
||||
unit: '',
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
}
|
||||
|
||||
const formData = convertToInputFieldFormData(inputVar)
|
||||
|
||||
expect(formData.default).toBe('')
|
||||
expect(formData.tooltips).toBe('')
|
||||
expect(formData.placeholder).toBe('')
|
||||
expect(formData.unit).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multiple fields workflow', () => {
|
||||
it('should process multiple fields independently', async () => {
|
||||
const { convertToInputFieldFormData, convertFormDataToINputField } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
|
||||
)
|
||||
|
||||
const fields: InputVar[] = [
|
||||
{
|
||||
type: PipelineInputVarType.textInput,
|
||||
label: 'Name',
|
||||
variable: 'name',
|
||||
max_length: 48,
|
||||
default_value: 'Alice',
|
||||
required: true,
|
||||
tooltips: undefined,
|
||||
options: [],
|
||||
placeholder: undefined,
|
||||
unit: undefined,
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
},
|
||||
{
|
||||
type: PipelineInputVarType.number,
|
||||
label: 'Count',
|
||||
variable: 'count',
|
||||
max_length: 0,
|
||||
default_value: '10',
|
||||
required: false,
|
||||
tooltips: undefined,
|
||||
options: [],
|
||||
placeholder: undefined,
|
||||
unit: 'items',
|
||||
allowed_file_upload_methods: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
const formDataList = fields.map(f => convertToInputFieldFormData(f))
|
||||
const restoredFields = formDataList.map(fd => convertFormDataToINputField(fd))
|
||||
|
||||
expect(restoredFields).toHaveLength(2)
|
||||
expect(restoredFields[0].variable).toBe('name')
|
||||
expect(restoredFields[0].default_value).toBe('Alice')
|
||||
expect(restoredFields[1].variable).toBe('count')
|
||||
expect(restoredFields[1].default_value).toBe('10')
|
||||
expect(restoredFields[1].unit).toBe('items')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -178,7 +178,7 @@ describe('Input Field Editor Data Flow', () => {
|
||||
|
||||
expect(restored.type).toBe('number')
|
||||
expect(restored.unit).toBe('°C')
|
||||
expect(restored.default_value).toBe(0.7)
|
||||
expect(restored.default_value).toBe('0.7')
|
||||
})
|
||||
|
||||
it('should preserve select options through roundtrip', () => {
|
||||
|
||||
282
web/__tests__/rag-pipeline/test-run-flow.test.ts
Normal file
282
web/__tests__/rag-pipeline/test-run-flow.test.ts
Normal file
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Integration test: Test run end-to-end flow
|
||||
*
|
||||
* Validates the data flow through test-run preparation hooks:
|
||||
* step navigation, datasource filtering, and data clearing.
|
||||
*/
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Use string literals inside vi.hoisted to avoid import-before-init
|
||||
// BlockEnum.DataSource = 'datasource', BlockEnum.KnowledgeBase = 'knowledge-base'
|
||||
const mockNodes = vi.hoisted(() => [
|
||||
{
|
||||
id: 'ds-1',
|
||||
data: {
|
||||
type: 'datasource',
|
||||
title: 'Local Files',
|
||||
datasource_type: 'upload_file',
|
||||
datasource_configurations: { datasource_label: 'Upload', upload_file_config: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ds-2',
|
||||
data: {
|
||||
type: 'datasource',
|
||||
title: 'Web Crawl',
|
||||
datasource_type: 'website_crawl',
|
||||
datasource_configurations: { datasource_label: 'Crawl' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'kb-1',
|
||||
data: {
|
||||
type: 'knowledge-base',
|
||||
title: 'Knowledge Base',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useNodes: () => mockNodes,
|
||||
}))
|
||||
|
||||
// Mock the Zustand store used by the hooks
|
||||
const mockSetDocumentsData = vi.fn()
|
||||
const mockSetSearchValue = vi.fn()
|
||||
const mockSetSelectedPagesId = vi.fn()
|
||||
const mockSetOnlineDocuments = vi.fn()
|
||||
const mockSetCurrentDocument = vi.fn()
|
||||
const mockSetStep = vi.fn()
|
||||
const mockSetCrawlResult = vi.fn()
|
||||
const mockSetWebsitePages = vi.fn()
|
||||
const mockSetPreviewIndex = vi.fn()
|
||||
const mockSetCurrentWebsite = vi.fn()
|
||||
const mockSetOnlineDriveFileList = vi.fn()
|
||||
const mockSetBucket = vi.fn()
|
||||
const mockSetPrefix = vi.fn()
|
||||
const mockSetKeywords = vi.fn()
|
||||
const mockSetSelectedFileIds = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/store', () => ({
|
||||
useDataSourceStore: () => ({
|
||||
getState: () => ({
|
||||
setDocumentsData: mockSetDocumentsData,
|
||||
setSearchValue: mockSetSearchValue,
|
||||
setSelectedPagesId: mockSetSelectedPagesId,
|
||||
setOnlineDocuments: mockSetOnlineDocuments,
|
||||
setCurrentDocument: mockSetCurrentDocument,
|
||||
setStep: mockSetStep,
|
||||
setCrawlResult: mockSetCrawlResult,
|
||||
setWebsitePages: mockSetWebsitePages,
|
||||
setPreviewIndex: mockSetPreviewIndex,
|
||||
setCurrentWebsite: mockSetCurrentWebsite,
|
||||
setOnlineDriveFileList: mockSetOnlineDriveFileList,
|
||||
setBucket: mockSetBucket,
|
||||
setPrefix: mockSetPrefix,
|
||||
setKeywords: mockSetKeywords,
|
||||
setSelectedFileIds: mockSetSelectedFileIds,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/rag-pipeline/components/panel/test-run/types', () => ({
|
||||
TestRunStep: {
|
||||
dataSource: 'data_source',
|
||||
documentProcessing: 'document_processing',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/models/datasets', () => ({
|
||||
CrawlStep: {
|
||||
init: 'init',
|
||||
},
|
||||
}))
|
||||
|
||||
describe('Test Run Flow Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Step Navigation', () => {
|
||||
it('should start at step 1 and navigate forward', async () => {
|
||||
const { useTestRunSteps } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useTestRunSteps())
|
||||
|
||||
expect(result.current.currentStep).toBe(1)
|
||||
|
||||
act(() => {
|
||||
result.current.handleNextStep()
|
||||
})
|
||||
|
||||
expect(result.current.currentStep).toBe(2)
|
||||
})
|
||||
|
||||
it('should navigate back from step 2 to step 1', async () => {
|
||||
const { useTestRunSteps } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useTestRunSteps())
|
||||
|
||||
act(() => {
|
||||
result.current.handleNextStep()
|
||||
})
|
||||
expect(result.current.currentStep).toBe(2)
|
||||
|
||||
act(() => {
|
||||
result.current.handleBackStep()
|
||||
})
|
||||
expect(result.current.currentStep).toBe(1)
|
||||
})
|
||||
|
||||
it('should provide labeled steps', async () => {
|
||||
const { useTestRunSteps } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useTestRunSteps())
|
||||
|
||||
expect(result.current.steps).toHaveLength(2)
|
||||
expect(result.current.steps[0].value).toBe('data_source')
|
||||
expect(result.current.steps[1].value).toBe('document_processing')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Datasource Options', () => {
|
||||
it('should filter nodes to only DataSource type', async () => {
|
||||
const { useDatasourceOptions } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useDatasourceOptions())
|
||||
|
||||
// Should only include DataSource nodes, not KnowledgeBase
|
||||
expect(result.current).toHaveLength(2)
|
||||
expect(result.current[0].value).toBe('ds-1')
|
||||
expect(result.current[1].value).toBe('ds-2')
|
||||
})
|
||||
|
||||
it('should include node data in options', async () => {
|
||||
const { useDatasourceOptions } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useDatasourceOptions())
|
||||
|
||||
expect(result.current[0].label).toBe('Local Files')
|
||||
expect(result.current[0].data.type).toBe('datasource')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data Clearing Flow', () => {
|
||||
it('should clear online document data', async () => {
|
||||
const { useOnlineDocument } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useOnlineDocument())
|
||||
|
||||
act(() => {
|
||||
result.current.clearOnlineDocumentData()
|
||||
})
|
||||
|
||||
expect(mockSetDocumentsData).toHaveBeenCalledWith([])
|
||||
expect(mockSetSearchValue).toHaveBeenCalledWith('')
|
||||
expect(mockSetSelectedPagesId).toHaveBeenCalledWith(expect.any(Set))
|
||||
expect(mockSetOnlineDocuments).toHaveBeenCalledWith([])
|
||||
expect(mockSetCurrentDocument).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
|
||||
it('should clear website crawl data', async () => {
|
||||
const { useWebsiteCrawl } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useWebsiteCrawl())
|
||||
|
||||
act(() => {
|
||||
result.current.clearWebsiteCrawlData()
|
||||
})
|
||||
|
||||
expect(mockSetStep).toHaveBeenCalledWith('init')
|
||||
expect(mockSetCrawlResult).toHaveBeenCalledWith(undefined)
|
||||
expect(mockSetCurrentWebsite).toHaveBeenCalledWith(undefined)
|
||||
expect(mockSetWebsitePages).toHaveBeenCalledWith([])
|
||||
expect(mockSetPreviewIndex).toHaveBeenCalledWith(-1)
|
||||
})
|
||||
|
||||
it('should clear online drive data', async () => {
|
||||
const { useOnlineDrive } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useOnlineDrive())
|
||||
|
||||
act(() => {
|
||||
result.current.clearOnlineDriveData()
|
||||
})
|
||||
|
||||
expect(mockSetOnlineDriveFileList).toHaveBeenCalledWith([])
|
||||
expect(mockSetBucket).toHaveBeenCalledWith('')
|
||||
expect(mockSetPrefix).toHaveBeenCalledWith([])
|
||||
expect(mockSetKeywords).toHaveBeenCalledWith('')
|
||||
expect(mockSetSelectedFileIds).toHaveBeenCalledWith([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Full Flow Simulation', () => {
|
||||
it('should support complete step navigation cycle', async () => {
|
||||
const { useTestRunSteps } = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result } = renderHook(() => useTestRunSteps())
|
||||
|
||||
// Start at step 1
|
||||
expect(result.current.currentStep).toBe(1)
|
||||
|
||||
// Move to step 2
|
||||
act(() => {
|
||||
result.current.handleNextStep()
|
||||
})
|
||||
expect(result.current.currentStep).toBe(2)
|
||||
|
||||
// Go back to step 1
|
||||
act(() => {
|
||||
result.current.handleBackStep()
|
||||
})
|
||||
expect(result.current.currentStep).toBe(1)
|
||||
|
||||
// Move forward again
|
||||
act(() => {
|
||||
result.current.handleNextStep()
|
||||
})
|
||||
expect(result.current.currentStep).toBe(2)
|
||||
})
|
||||
|
||||
it('should not regress when clearing all data sources in sequence', async () => {
|
||||
const {
|
||||
useOnlineDocument,
|
||||
useWebsiteCrawl,
|
||||
useOnlineDrive,
|
||||
} = await import(
|
||||
'@/app/components/rag-pipeline/components/panel/test-run/preparation/hooks',
|
||||
)
|
||||
const { result: docResult } = renderHook(() => useOnlineDocument())
|
||||
const { result: crawlResult } = renderHook(() => useWebsiteCrawl())
|
||||
const { result: driveResult } = renderHook(() => useOnlineDrive())
|
||||
|
||||
// Clear all data sources
|
||||
act(() => {
|
||||
docResult.current.clearOnlineDocumentData()
|
||||
crawlResult.current.clearWebsiteCrawlData()
|
||||
driveResult.current.clearOnlineDriveData()
|
||||
})
|
||||
|
||||
expect(mockSetDocumentsData).toHaveBeenCalledWith([])
|
||||
expect(mockSetStep).toHaveBeenCalledWith('init')
|
||||
expect(mockSetOnlineDriveFileList).toHaveBeenCalledWith([])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,218 @@
|
||||
import type { ParentChildChunk } from './types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
|
||||
import ChunkCard from './chunk-card'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: Record<string, unknown>) => `${key}${opts?.count !== undefined ? `:${opts.count}` : ''}`,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/dot', () => ({
|
||||
default: () => <span data-testid="dot" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/segment-index-tag', () => ({
|
||||
default: ({ positionId, labelPrefix }: { positionId?: string | number, labelPrefix: string }) => (
|
||||
<span data-testid="segment-tag">
|
||||
{labelPrefix}
|
||||
-
|
||||
{positionId}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/summary-label', () => ({
|
||||
default: ({ summary }: { summary: string }) => <span data-testid="summary">{summary}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/formatted-text/flavours/preview-slice', () => ({
|
||||
PreviewSlice: ({ label, text }: { label: string, text: string }) => (
|
||||
<span data-testid="preview-slice">
|
||||
{label}
|
||||
:
|
||||
{' '}
|
||||
{text}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/models/datasets', () => ({
|
||||
ChunkingMode: {
|
||||
text: 'text',
|
||||
parentChild: 'parent-child',
|
||||
qa: 'qa',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (n: number) => String(n),
|
||||
}))
|
||||
|
||||
vi.mock('./q-a-item', () => ({
|
||||
default: ({ type, text }: { type: string, text: string }) => (
|
||||
<span data-testid={`qa-${type}`}>{text}</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./types', () => ({
|
||||
QAItemType: {
|
||||
Question: 'question',
|
||||
Answer: 'answer',
|
||||
},
|
||||
}))
|
||||
|
||||
const makeParentChildContent = (overrides: Partial<ParentChildChunk> = {}): ParentChildChunk => ({
|
||||
child_contents: ['Child'],
|
||||
parent_content: '',
|
||||
parent_summary: '',
|
||||
parent_mode: 'paragraph',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('ChunkCard', () => {
|
||||
describe('Text mode', () => {
|
||||
it('should render text content', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
content={{ content: 'Hello world', summary: 'Summary text' }}
|
||||
positionId={1}
|
||||
wordCount={42}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Hello world')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render segment index tag with Chunk prefix', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
content={{ content: 'Test', summary: '' }}
|
||||
positionId={5}
|
||||
wordCount={10}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Chunk-5')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render word count', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
content={{ content: 'Test', summary: '' }}
|
||||
positionId={1}
|
||||
wordCount={100}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/100/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render summary when available', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
content={{ content: 'Test', summary: 'A summary' }}
|
||||
positionId={1}
|
||||
wordCount={10}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('summary')).toHaveTextContent('A summary')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parent-Child mode (paragraph)', () => {
|
||||
it('should render child contents as preview slices', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
parentMode="paragraph"
|
||||
content={makeParentChildContent({
|
||||
child_contents: ['Child 1', 'Child 2'],
|
||||
parent_summary: 'Parent summary',
|
||||
})}
|
||||
positionId={3}
|
||||
wordCount={50}
|
||||
/>,
|
||||
)
|
||||
|
||||
const slices = screen.getAllByTestId('preview-slice')
|
||||
expect(slices).toHaveLength(2)
|
||||
expect(slices[0]).toHaveTextContent('C-1: Child 1')
|
||||
expect(slices[1]).toHaveTextContent('C-2: Child 2')
|
||||
})
|
||||
|
||||
it('should render Parent-Chunk prefix for paragraph mode', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
parentMode="paragraph"
|
||||
content={makeParentChildContent()}
|
||||
positionId={2}
|
||||
wordCount={20}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Parent-Chunk-2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render parent summary', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
parentMode="paragraph"
|
||||
content={makeParentChildContent({
|
||||
child_contents: ['C1'],
|
||||
parent_summary: 'Overview',
|
||||
})}
|
||||
positionId={1}
|
||||
wordCount={10}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('summary')).toHaveTextContent('Overview')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parent-Child mode (full-doc)', () => {
|
||||
it('should hide segment tag in full-doc mode', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
parentMode="full-doc"
|
||||
content={makeParentChildContent({
|
||||
child_contents: ['Full doc child'],
|
||||
parent_mode: 'full-doc',
|
||||
})}
|
||||
positionId={1}
|
||||
wordCount={300}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('segment-tag')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('QA mode', () => {
|
||||
it('should render question and answer items', () => {
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.qa}
|
||||
content={{ question: 'What is X?', answer: 'X is Y' }}
|
||||
positionId={1}
|
||||
wordCount={15}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('qa-question')).toHaveTextContent('What is X?')
|
||||
expect(screen.getByTestId('qa-answer')).toHaveTextContent('X is Y')
|
||||
})
|
||||
})
|
||||
})
|
||||
189
web/app/components/rag-pipeline/components/conversion.spec.tsx
Normal file
189
web/app/components/rag-pipeline/components/conversion.spec.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Conversion from './conversion'
|
||||
|
||||
const mockConvert = vi.fn()
|
||||
const mockInvalidDatasetDetail = vi.fn()
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'ds-123' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useConvertDatasetToPipeline: () => ({
|
||||
mutateAsync: mockConvert,
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
datasetDetailQueryKeyPrefix: ['dataset-detail'],
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-base', () => ({
|
||||
useInvalid: () => mockInvalidDatasetDetail,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, ...props }: Record<string, unknown>) => (
|
||||
<button onClick={onClick as () => void} {...props}>{children as string}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/confirm', () => ({
|
||||
default: ({
|
||||
isShow,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
title,
|
||||
}: {
|
||||
isShow: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
title: string
|
||||
}) =>
|
||||
isShow
|
||||
? (
|
||||
<div data-testid="confirm-modal">
|
||||
<span>{title}</span>
|
||||
<button data-testid="confirm-btn" onClick={onConfirm}>Confirm</button>
|
||||
<button data-testid="cancel-btn" onClick={onCancel}>Cancel</button>
|
||||
</div>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('./screenshot', () => ({
|
||||
default: () => <div data-testid="screenshot" />,
|
||||
}))
|
||||
|
||||
describe('Conversion', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should render conversion title and description', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByText('conversion.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversion.descriptionChunk1')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversion.descriptionChunk2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render convert button', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByText('operations.convert')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render warning text', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByText('conversion.warning')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render screenshot component', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByTestId('screenshot')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show confirm modal when convert button clicked', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
|
||||
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversion.confirm.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide confirm modal when cancel is clicked', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call convert when confirm is clicked', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(mockConvert).toHaveBeenCalledWith('ds-123', expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle successful conversion', async () => {
|
||||
const Toast = await import('@/app/components/base/toast')
|
||||
mockConvert.mockImplementation((_id: string, opts: { onSuccess: (res: { status: string }) => void }) => {
|
||||
opts.onSuccess({ status: 'success' })
|
||||
})
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
}))
|
||||
expect(mockInvalidDatasetDetail).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle failed conversion', async () => {
|
||||
const Toast = await import('@/app/components/base/toast')
|
||||
mockConvert.mockImplementation((_id: string, opts: { onSuccess: (res: { status: string }) => void }) => {
|
||||
opts.onSuccess({ status: 'failed' })
|
||||
})
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle conversion error', async () => {
|
||||
const Toast = await import('@/app/components/base/toast')
|
||||
mockConvert.mockImplementation((_id: string, opts: { onError: () => void }) => {
|
||||
opts.onError()
|
||||
})
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
}))
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,251 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import PublishAsKnowledgePipelineModal from './publish-as-knowledge-pipeline-modal'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
knowledgeName: 'Test Pipeline',
|
||||
knowledgeIcon: {
|
||||
icon_type: 'emoji',
|
||||
icon: '🔧',
|
||||
icon_background: '#fff',
|
||||
icon_url: '',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/modal', () => ({
|
||||
default: ({ children, isShow }: { children: React.ReactNode, isShow: boolean }) =>
|
||||
isShow ? <div data-testid="modal">{children}</div> : null,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, disabled, ...props }: Record<string, unknown>) => (
|
||||
<button onClick={onClick as () => void} disabled={disabled as boolean} {...props}>
|
||||
{children as string}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/input', () => ({
|
||||
default: ({ value, onChange, ...props }: Record<string, unknown>) => (
|
||||
<input
|
||||
data-testid="name-input"
|
||||
value={value as string}
|
||||
onChange={onChange as () => void}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/textarea', () => ({
|
||||
default: ({ value, onChange, ...props }: Record<string, unknown>) => (
|
||||
<textarea
|
||||
data-testid="description-textarea"
|
||||
value={value as string}
|
||||
onChange={onChange as () => void}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ onClick }: { onClick?: () => void }) => (
|
||||
<div data-testid="app-icon" onClick={onClick} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon-picker', () => ({
|
||||
default: ({ onSelect, onClose }: { onSelect: (item: { type: string, icon: string, background: string, url: string }) => void, onClose: () => void }) => (
|
||||
<div data-testid="icon-picker">
|
||||
<button data-testid="select-emoji" onClick={() => onSelect({ type: 'emoji', icon: '🎉', background: '#eee', url: '' })}>
|
||||
Select Emoji
|
||||
</button>
|
||||
<button data-testid="select-image" onClick={() => onSelect({ type: 'image', icon: '', background: '', url: 'http://img.png' })}>
|
||||
Select Image
|
||||
</button>
|
||||
<button data-testid="close-picker" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('es-toolkit/function', () => ({
|
||||
noop: () => {},
|
||||
}))
|
||||
|
||||
describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnConfirm = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
const defaultProps = {
|
||||
onCancel: mockOnCancel,
|
||||
onConfirm: mockOnConfirm,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should render modal with title', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.publishAs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should initialize with knowledgeName from store', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const nameInput = screen.getByTestId('name-input') as HTMLInputElement
|
||||
expect(nameInput.value).toBe('Test Pipeline')
|
||||
})
|
||||
|
||||
it('should initialize description as empty', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const textarea = screen.getByTestId('description-textarea') as HTMLTextAreaElement
|
||||
expect(textarea.value).toBe('')
|
||||
})
|
||||
|
||||
it('should call onCancel when close button clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('publish-modal-close-btn'))
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onCancel when cancel button clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('operation.cancel'))
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onConfirm with name, icon, and description when confirm clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.publish'))
|
||||
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(
|
||||
'Test Pipeline',
|
||||
expect.objectContaining({ icon_type: 'emoji', icon: '🔧' }),
|
||||
'',
|
||||
)
|
||||
})
|
||||
|
||||
it('should update pipeline name when input changes', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const nameInput = screen.getByTestId('name-input')
|
||||
fireEvent.change(nameInput, { target: { value: 'New Name' } })
|
||||
|
||||
expect((nameInput as HTMLInputElement).value).toBe('New Name')
|
||||
})
|
||||
|
||||
it('should update description when textarea changes', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
fireEvent.change(textarea, { target: { value: 'My description' } })
|
||||
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe('My description')
|
||||
})
|
||||
|
||||
it('should disable confirm button when name is empty', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const nameInput = screen.getByTestId('name-input')
|
||||
fireEvent.change(nameInput, { target: { value: '' } })
|
||||
|
||||
const confirmBtn = screen.getByText('common.publish')
|
||||
expect(confirmBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable confirm button when confirmDisabled is true', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} confirmDisabled />)
|
||||
|
||||
const confirmBtn = screen.getByText('common.publish')
|
||||
expect(confirmBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not call onConfirm when confirmDisabled is true', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} confirmDisabled />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.publish'))
|
||||
|
||||
expect(mockOnConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show icon picker when app icon clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
|
||||
expect(screen.getByTestId('icon-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update icon when emoji is selected', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
fireEvent.click(screen.getByTestId('select-emoji'))
|
||||
|
||||
// Icon picker should close
|
||||
expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update icon when image is selected', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
fireEvent.click(screen.getByTestId('select-image'))
|
||||
|
||||
expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close icon picker when close is clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
fireEvent.click(screen.getByTestId('close-picker'))
|
||||
|
||||
expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should trim name and description before submitting', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const nameInput = screen.getByTestId('name-input')
|
||||
fireEvent.change(nameInput, { target: { value: ' Trimmed Name ' } })
|
||||
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
fireEvent.change(textarea, { target: { value: ' Some desc ' } })
|
||||
|
||||
fireEvent.click(screen.getByText('common.publish'))
|
||||
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(
|
||||
'Trimmed Name',
|
||||
expect.any(Object),
|
||||
'Some desc',
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,328 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Popup from './popup'
|
||||
|
||||
const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
|
||||
const mockPublishAsCustomizedPipeline = vi.fn().mockResolvedValue({})
|
||||
const mockNotify = vi.fn()
|
||||
const mockPush = vi.fn()
|
||||
const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true)
|
||||
const mockSetPublishedAt = vi.fn()
|
||||
const mockMutateDatasetRes = vi.fn()
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockInvalidPublishedPipelineInfo = vi.fn()
|
||||
const mockInvalidDatasetList = vi.fn()
|
||||
const mockInvalidCustomizedTemplateList = vi.fn()
|
||||
|
||||
let mockPublishedAt: string | undefined = '2024-01-01T00:00:00Z'
|
||||
let mockDraftUpdatedAt: string | undefined = '2024-06-01T00:00:00Z'
|
||||
let mockPipelineId: string | undefined = 'pipeline-123'
|
||||
let mockIsAllowPublishAsCustom = true
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'ds-123' }),
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode, href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useBoolean: (initial: boolean) => {
|
||||
// Simple implementation for testing
|
||||
const state = { value: initial }
|
||||
return [state.value, {
|
||||
setFalse: vi.fn(),
|
||||
setTrue: vi.fn(),
|
||||
}]
|
||||
},
|
||||
useKeyPress: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
publishedAt: mockPublishedAt,
|
||||
draftUpdatedAt: mockDraftUpdatedAt,
|
||||
pipelineId: mockPipelineId,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setPublishedAt: mockSetPublishedAt,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, disabled, variant, className }: Record<string, unknown>) => (
|
||||
<button
|
||||
onClick={onClick as () => void}
|
||||
disabled={disabled as boolean}
|
||||
data-variant={variant as string}
|
||||
className={className as string}
|
||||
>
|
||||
{children as React.ReactNode}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/confirm', () => ({
|
||||
default: ({ isShow, onConfirm, onCancel, title }: {
|
||||
isShow: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
title: string
|
||||
}) =>
|
||||
isShow
|
||||
? (
|
||||
<div data-testid="confirm-modal">
|
||||
<span>{title}</span>
|
||||
<button data-testid="publish-confirm" onClick={onConfirm}>OK</button>
|
||||
<button data-testid="publish-cancel" onClick={onCancel}>Cancel</button>
|
||||
</div>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <hr />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/public/common', () => ({
|
||||
SparklesSoft: () => <span data-testid="sparkles" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/premium-badge', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <span data-testid="premium-badge">{children}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useChecklistBeforePublish: () => ({
|
||||
handleCheckBeforePublish: mockHandleCheckBeforePublish,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/shortcuts-name', () => ({
|
||||
default: ({ keys }: { keys: string[] }) => <span data-testid="shortcuts">{keys.join('+')}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
getKeyboardKeyCodeBySystem: () => 'ctrl',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: () => mockMutateDatasetRes,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => () => 'https://docs.dify.ai',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: () => mockSetShowPricingModal,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: () => mockIsAllowPublishAsCustom,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-api-access-url', () => ({
|
||||
useDatasetApiAccessUrl: () => '/api/datasets/ds-123',
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||
useFormatTimeFromNow: () => ({
|
||||
formatTimeFromNow: (time: string) => `formatted:${time}`,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useInvalidDatasetList: () => mockInvalidDatasetList,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-base', () => ({
|
||||
useInvalid: () => mockInvalidPublishedPipelineInfo,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
publishedPipelineInfoQueryKeyPrefix: ['published-pipeline'],
|
||||
useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList,
|
||||
usePublishAsCustomizedPipeline: () => ({
|
||||
mutateAsync: mockPublishAsCustomizedPipeline,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
usePublishWorkflow: () => ({
|
||||
mutateAsync: mockPublishWorkflow,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: string[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({
|
||||
default: ({ onConfirm, onCancel }: { onConfirm: (name: string, icon: unknown, desc: string) => void, onCancel: () => void }) => (
|
||||
<div data-testid="publish-as-modal">
|
||||
<button data-testid="publish-as-confirm" onClick={() => onConfirm('My Pipeline', { icon_type: 'emoji' }, 'desc')}>
|
||||
Confirm
|
||||
</button>
|
||||
<button data-testid="publish-as-cancel" onClick={onCancel}>Cancel</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiArrowRightUpLine: () => <span />,
|
||||
RiHammerLine: () => <span />,
|
||||
RiPlayCircleLine: () => <span />,
|
||||
RiTerminalBoxLine: () => <span />,
|
||||
}))
|
||||
|
||||
describe('Popup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPublishedAt = '2024-01-01T00:00:00Z'
|
||||
mockDraftUpdatedAt = '2024-06-01T00:00:00Z'
|
||||
mockPipelineId = 'pipeline-123'
|
||||
mockIsAllowPublishAsCustom = true
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render when published', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.latestPublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/common.publishedAt/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render unpublished state', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.currentDraftUnpublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/common.autoSaved/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render publish button with shortcuts', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.publishUpdate')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('shortcuts')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "Go to Add Documents" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.goToAddDocuments')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "API Reference" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.accessAPIReference')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "Publish As" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.publishAs')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Premium Badge', () => {
|
||||
it('should not show premium badge when allowed', () => {
|
||||
mockIsAllowPublishAsCustom = true
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.queryByTestId('premium-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show premium badge when not allowed', () => {
|
||||
mockIsAllowPublishAsCustom = false
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByTestId('premium-badge')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should navigate to add documents page', () => {
|
||||
render(<Popup />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.goToAddDocuments'))
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/ds-123/documents/create-from-pipeline')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Button disable states', () => {
|
||||
it('should disable add documents button when not published', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
const btn = screen.getByText('common.goToAddDocuments').closest('button')
|
||||
expect(btn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable publish-as button when not published', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
const btn = screen.getByText('common.publishAs').closest('button')
|
||||
expect(btn).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Publish As Knowledge Pipeline', () => {
|
||||
it('should show pricing modal when not allowed', () => {
|
||||
mockIsAllowPublishAsCustom = false
|
||||
render(<Popup />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.publishAs'))
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Time formatting', () => {
|
||||
it('should format published time', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText(/formatted:2024-01-01/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format draft updated time when unpublished', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText(/formatted:2024-06-01/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,199 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import RunMode from './run-mode'
|
||||
|
||||
const mockHandleWorkflowStartRunInWorkflow = vi.fn()
|
||||
const mockHandleStopRun = vi.fn()
|
||||
const mockSetIsPreparingDataSource = vi.fn()
|
||||
const mockSetShowDebugAndPreviewPanel = vi.fn()
|
||||
|
||||
let mockWorkflowRunningData: { task_id: string, result: { status: string } } | undefined
|
||||
let mockIsPreparingDataSource = false
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowRun: () => ({
|
||||
handleStopRun: mockHandleStopRun,
|
||||
}),
|
||||
useWorkflowStartRun: () => ({
|
||||
handleWorkflowStartRunInWorkflow: mockHandleWorkflowStartRunInWorkflow,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/shortcuts-name', () => ({
|
||||
default: ({ keys }: { keys: string[] }) => <span data-testid="shortcuts">{keys.join('+')}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
workflowRunningData: mockWorkflowRunningData,
|
||||
isPreparingDataSource: mockIsPreparingDataSource,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
setIsPreparingDataSource: mockSetIsPreparingDataSource,
|
||||
setShowDebugAndPreviewPanel: mockSetShowDebugAndPreviewPanel,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/types', () => ({
|
||||
WorkflowRunningStatus: { Running: 'running' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/variable-inspect/types', () => ({
|
||||
EVENT_WORKFLOW_STOP: 'EVENT_WORKFLOW_STOP',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: { useSubscription: vi.fn() },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(a => typeof a === 'string').join(' '),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiCloseLine: () => <span data-testid="close-icon" />,
|
||||
RiDatabase2Line: () => <span data-testid="database-icon" />,
|
||||
RiLoader2Line: () => <span data-testid="loader-icon" />,
|
||||
RiPlayLargeLine: () => <span data-testid="play-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/line/mediaAndDevices', () => ({
|
||||
StopCircle: () => <span data-testid="stop-icon" />,
|
||||
}))
|
||||
|
||||
describe('RunMode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkflowRunningData = undefined
|
||||
mockIsPreparingDataSource = false
|
||||
})
|
||||
|
||||
describe('Idle state', () => {
|
||||
it('should render test run text when no data', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.testRun')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom text when provided', () => {
|
||||
render(<RunMode text="Custom Run" />)
|
||||
|
||||
expect(screen.getByText('Custom Run')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render play icon', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByTestId('play-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render keyboard shortcuts', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByTestId('shortcuts')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call start run when button clicked', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.testRun'))
|
||||
|
||||
expect(mockHandleWorkflowStartRunInWorkflow).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Running state', () => {
|
||||
beforeEach(() => {
|
||||
mockWorkflowRunningData = {
|
||||
task_id: 'task-1',
|
||||
result: { status: 'running' },
|
||||
}
|
||||
})
|
||||
|
||||
it('should show processing text', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.processing')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show stop button', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByTestId('stop-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable run button', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
const button = screen.getByText('common.processing').closest('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should call handleStopRun with task_id when stop clicked', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('stop-icon').closest('button')!)
|
||||
|
||||
expect(mockHandleStopRun).toHaveBeenCalledWith('task-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('After run completed', () => {
|
||||
it('should show reRun text when previous run data exists', () => {
|
||||
mockWorkflowRunningData = {
|
||||
task_id: 'task-1',
|
||||
result: { status: 'succeeded' },
|
||||
}
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.reRun')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Preparing data source state', () => {
|
||||
beforeEach(() => {
|
||||
mockIsPreparingDataSource = true
|
||||
})
|
||||
|
||||
it('should show preparing text', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.preparingDataSource')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show database icon', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByTestId('database-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show cancel button with close icon', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByTestId('close-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should cancel preparing when close clicked', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('close-icon').closest('button')!)
|
||||
|
||||
expect(mockSetIsPreparingDataSource).toHaveBeenCalledWith(false)
|
||||
expect(mockSetShowDebugAndPreviewPanel).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,136 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useAvailableNodesMetaData } from './use-available-nodes-meta-data'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path?: string) => `https://docs.dify.ai${path || ''}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants/node', () => ({
|
||||
WORKFLOW_COMMON_NODES: [
|
||||
{
|
||||
metaData: { type: BlockEnum.LLM },
|
||||
defaultValue: { title: 'LLM' },
|
||||
},
|
||||
{
|
||||
metaData: { type: BlockEnum.HumanInput },
|
||||
defaultValue: { title: 'Human Input' },
|
||||
},
|
||||
{
|
||||
metaData: { type: BlockEnum.HttpRequest },
|
||||
defaultValue: { title: 'HTTP Request' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/data-source-empty/default', () => ({
|
||||
default: {
|
||||
metaData: { type: BlockEnum.DataSourceEmpty },
|
||||
defaultValue: { title: 'Data Source Empty' },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/data-source/default', () => ({
|
||||
default: {
|
||||
metaData: { type: BlockEnum.DataSource },
|
||||
defaultValue: { title: 'Data Source' },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/knowledge-base/default', () => ({
|
||||
default: {
|
||||
metaData: { type: BlockEnum.KnowledgeBase },
|
||||
defaultValue: { title: 'Knowledge Base' },
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useAvailableNodesMetaData', () => {
|
||||
it('should return nodes and nodesMap', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
expect(result.current.nodes).toBeDefined()
|
||||
expect(result.current.nodesMap).toBeDefined()
|
||||
})
|
||||
|
||||
it('should filter out HumanInput node', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const nodeTypes = result.current.nodes.map(n => n.metaData.type)
|
||||
|
||||
expect(nodeTypes).not.toContain(BlockEnum.HumanInput)
|
||||
})
|
||||
|
||||
it('should include DataSource with _dataSourceStartToAdd flag', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const dsNode = result.current.nodes.find(n => n.metaData.type === BlockEnum.DataSource)
|
||||
|
||||
expect(dsNode).toBeDefined()
|
||||
expect(dsNode!.defaultValue._dataSourceStartToAdd).toBe(true)
|
||||
})
|
||||
|
||||
it('should include KnowledgeBase and DataSourceEmpty nodes', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const nodeTypes = result.current.nodes.map(n => n.metaData.type)
|
||||
|
||||
expect(nodeTypes).toContain(BlockEnum.KnowledgeBase)
|
||||
expect(nodeTypes).toContain(BlockEnum.DataSourceEmpty)
|
||||
})
|
||||
|
||||
it('should translate title and description for each node', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
result.current.nodes.forEach((node) => {
|
||||
expect(node.metaData.title).toMatch(/^blocks\./)
|
||||
expect(node.metaData.description).toMatch(/^blocksAbout\./)
|
||||
})
|
||||
})
|
||||
|
||||
it('should set helpLinkUri on each node metaData', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
result.current.nodes.forEach((node) => {
|
||||
expect(node.metaData.helpLinkUri).toContain('https://docs.dify.ai')
|
||||
expect(node.metaData.helpLinkUri).toContain('knowledge-pipeline')
|
||||
})
|
||||
})
|
||||
|
||||
it('should set type and title on defaultValue', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
result.current.nodes.forEach((node) => {
|
||||
expect(node.defaultValue.type).toBe(node.metaData.type)
|
||||
expect(node.defaultValue.title).toBe(node.metaData.title)
|
||||
})
|
||||
})
|
||||
|
||||
it('should build nodesMap indexed by BlockEnum type', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const { nodesMap } = result.current
|
||||
|
||||
expect(nodesMap[BlockEnum.LLM]).toBeDefined()
|
||||
expect(nodesMap[BlockEnum.DataSource]).toBeDefined()
|
||||
expect(nodesMap[BlockEnum.KnowledgeBase]).toBeDefined()
|
||||
})
|
||||
|
||||
it('should alias VariableAssigner to VariableAggregator in nodesMap', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const { nodesMap } = result.current
|
||||
|
||||
expect(nodesMap[BlockEnum.VariableAssigner]).toBe(nodesMap[BlockEnum.VariableAggregator])
|
||||
})
|
||||
|
||||
it('should include common nodes except HumanInput', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
const nodeTypes = result.current.nodes.map(n => n.metaData.type)
|
||||
|
||||
expect(nodeTypes).toContain(BlockEnum.LLM)
|
||||
expect(nodeTypes).toContain(BlockEnum.HttpRequest)
|
||||
expect(nodeTypes).not.toContain(BlockEnum.HumanInput)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,70 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useConfigsMap } from './use-configs-map'
|
||||
|
||||
const mockPipelineId = 'pipeline-xyz'
|
||||
const mockFileUploadConfig = { max_size: 10 }
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
pipelineId: mockPipelineId,
|
||||
fileUploadConfig: mockFileUploadConfig,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/types/app', () => ({
|
||||
Resolution: { high: 'high' },
|
||||
TransferMethod: { local_file: 'local_file', remote_url: 'remote_url' },
|
||||
}))
|
||||
|
||||
vi.mock('@/types/common', () => ({
|
||||
FlowType: { ragPipeline: 'rag-pipeline' },
|
||||
}))
|
||||
|
||||
describe('useConfigsMap', () => {
|
||||
it('should return flowId from pipelineId', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.flowId).toBe('pipeline-xyz')
|
||||
})
|
||||
|
||||
it('should return ragPipeline as flowType', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.flowType).toBe('rag-pipeline')
|
||||
})
|
||||
|
||||
it('should include file settings with image disabled', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.fileSettings.image.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('should set image detail to high resolution', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.fileSettings.image.detail).toBe('high')
|
||||
})
|
||||
|
||||
it('should set image number_limits to 3', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.fileSettings.image.number_limits).toBe(3)
|
||||
})
|
||||
|
||||
it('should include both transfer methods for image', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.fileSettings.image.transfer_methods).toEqual(['local_file', 'remote_url'])
|
||||
})
|
||||
|
||||
it('should pass through fileUploadConfig from store', () => {
|
||||
const { result } = renderHook(() => useConfigsMap())
|
||||
|
||||
expect(result.current.fileSettings.fileUploadConfig).toEqual({ max_size: 10 })
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,45 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useGetRunAndTraceUrl } from './use-get-run-and-trace-url'
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
pipelineId: 'pipeline-test-123',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useGetRunAndTraceUrl', () => {
|
||||
it('should return a function getWorkflowRunAndTraceUrl', () => {
|
||||
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
||||
|
||||
expect(typeof result.current.getWorkflowRunAndTraceUrl).toBe('function')
|
||||
})
|
||||
|
||||
it('should generate correct runUrl', () => {
|
||||
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
||||
const { runUrl } = result.current.getWorkflowRunAndTraceUrl('run-abc')
|
||||
|
||||
expect(runUrl).toBe('/rag/pipelines/pipeline-test-123/workflow-runs/run-abc')
|
||||
})
|
||||
|
||||
it('should generate correct traceUrl', () => {
|
||||
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
||||
const { traceUrl } = result.current.getWorkflowRunAndTraceUrl('run-abc')
|
||||
|
||||
expect(traceUrl).toBe('/rag/pipelines/pipeline-test-123/workflow-runs/run-abc/node-executions')
|
||||
})
|
||||
|
||||
it('should handle different runIds', () => {
|
||||
const { result } = renderHook(() => useGetRunAndTraceUrl())
|
||||
|
||||
const r1 = result.current.getWorkflowRunAndTraceUrl('id-1')
|
||||
const r2 = result.current.getWorkflowRunAndTraceUrl('id-2')
|
||||
|
||||
expect(r1.runUrl).toContain('id-1')
|
||||
expect(r2.runUrl).toContain('id-2')
|
||||
expect(r1.runUrl).not.toBe(r2.runUrl)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,130 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useInputFieldPanel } from './use-input-field-panel'
|
||||
|
||||
const mockSetShowInputFieldPanel = vi.fn()
|
||||
const mockSetShowInputFieldPreviewPanel = vi.fn()
|
||||
const mockSetInputFieldEditPanelProps = vi.fn()
|
||||
|
||||
let mockShowInputFieldPreviewPanel = false
|
||||
let mockInputFieldEditPanelProps: unknown = null
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
showInputFieldPreviewPanel: mockShowInputFieldPreviewPanel,
|
||||
setShowInputFieldPanel: mockSetShowInputFieldPanel,
|
||||
setShowInputFieldPreviewPanel: mockSetShowInputFieldPreviewPanel,
|
||||
setInputFieldEditPanelProps: mockSetInputFieldEditPanelProps,
|
||||
}),
|
||||
}),
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
showInputFieldPreviewPanel: mockShowInputFieldPreviewPanel,
|
||||
inputFieldEditPanelProps: mockInputFieldEditPanelProps,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useInputFieldPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockShowInputFieldPreviewPanel = false
|
||||
mockInputFieldEditPanelProps = null
|
||||
})
|
||||
|
||||
describe('isPreviewing', () => {
|
||||
it('should return false when preview panel is hidden', () => {
|
||||
mockShowInputFieldPreviewPanel = false
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
expect(result.current.isPreviewing).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when preview panel is shown', () => {
|
||||
mockShowInputFieldPreviewPanel = true
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
expect(result.current.isPreviewing).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isEditing', () => {
|
||||
it('should return false when no edit panel props', () => {
|
||||
mockInputFieldEditPanelProps = null
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
expect(result.current.isEditing).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when edit panel props exist', () => {
|
||||
mockInputFieldEditPanelProps = { onSubmit: vi.fn(), onClose: vi.fn() }
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
expect(result.current.isEditing).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('closeAllInputFieldPanels', () => {
|
||||
it('should close all panels and clear edit props', () => {
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
act(() => {
|
||||
result.current.closeAllInputFieldPanels()
|
||||
})
|
||||
|
||||
expect(mockSetShowInputFieldPanel).toHaveBeenCalledWith(false)
|
||||
expect(mockSetShowInputFieldPreviewPanel).toHaveBeenCalledWith(false)
|
||||
expect(mockSetInputFieldEditPanelProps).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggleInputFieldPreviewPanel', () => {
|
||||
it('should toggle preview panel from false to true', () => {
|
||||
mockShowInputFieldPreviewPanel = false
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
act(() => {
|
||||
result.current.toggleInputFieldPreviewPanel()
|
||||
})
|
||||
|
||||
expect(mockSetShowInputFieldPreviewPanel).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should toggle preview panel from true to false', () => {
|
||||
mockShowInputFieldPreviewPanel = true
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
act(() => {
|
||||
result.current.toggleInputFieldPreviewPanel()
|
||||
})
|
||||
|
||||
expect(mockSetShowInputFieldPreviewPanel).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggleInputFieldEditPanel', () => {
|
||||
it('should set edit panel props when given content', () => {
|
||||
const editContent = { onSubmit: vi.fn(), onClose: vi.fn() }
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
act(() => {
|
||||
result.current.toggleInputFieldEditPanel(editContent)
|
||||
})
|
||||
|
||||
expect(mockSetInputFieldEditPanelProps).toHaveBeenCalledWith(editContent)
|
||||
})
|
||||
|
||||
it('should clear edit panel props when given null', () => {
|
||||
const { result } = renderHook(() => useInputFieldPanel())
|
||||
|
||||
act(() => {
|
||||
result.current.toggleInputFieldEditPanel(null)
|
||||
})
|
||||
|
||||
expect(mockSetInputFieldEditPanelProps).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
221
web/app/components/rag-pipeline/hooks/use-input-fields.spec.ts
Normal file
221
web/app/components/rag-pipeline/hooks/use-input-fields.spec.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { useConfigurations, useInitialData } from './use-input-fields'
|
||||
|
||||
vi.mock('@/models/pipeline', () => ({
|
||||
VAR_TYPE_MAP: {
|
||||
'text-input': BaseFieldType.textInput,
|
||||
'paragraph': BaseFieldType.paragraph,
|
||||
'select': BaseFieldType.select,
|
||||
'number': BaseFieldType.numberInput,
|
||||
'checkbox': BaseFieldType.checkbox,
|
||||
'file': BaseFieldType.file,
|
||||
'file-list': BaseFieldType.fileList,
|
||||
},
|
||||
}))
|
||||
|
||||
const makeVariable = (overrides: Record<string, unknown> = {}) => ({
|
||||
variable: 'test_var',
|
||||
label: 'Test Variable',
|
||||
type: 'text-input',
|
||||
required: true,
|
||||
max_length: 100,
|
||||
options: undefined,
|
||||
placeholder: '',
|
||||
tooltips: '',
|
||||
unit: '',
|
||||
default_value: undefined,
|
||||
allowed_file_types: undefined,
|
||||
allowed_file_extensions: undefined,
|
||||
allowed_file_upload_methods: undefined,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('useInitialData', () => {
|
||||
it('should initialize text-input with empty string by default', () => {
|
||||
const variables = [makeVariable({ type: 'text-input' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.test_var).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize paragraph with empty string by default', () => {
|
||||
const variables = [makeVariable({ type: 'paragraph', variable: 'para' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.para).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize select with empty string by default', () => {
|
||||
const variables = [makeVariable({ type: 'select', variable: 'sel' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.sel).toBe('')
|
||||
})
|
||||
|
||||
it('should initialize number with 0 by default', () => {
|
||||
const variables = [makeVariable({ type: 'number', variable: 'num' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.num).toBe(0)
|
||||
})
|
||||
|
||||
it('should initialize checkbox with false by default', () => {
|
||||
const variables = [makeVariable({ type: 'checkbox', variable: 'cb' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.cb).toBe(false)
|
||||
})
|
||||
|
||||
it('should initialize file with empty array by default', () => {
|
||||
const variables = [makeVariable({ type: 'file', variable: 'f' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.f).toEqual([])
|
||||
})
|
||||
|
||||
it('should initialize file-list with empty array by default', () => {
|
||||
const variables = [makeVariable({ type: 'file-list', variable: 'fl' })] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.fl).toEqual([])
|
||||
})
|
||||
|
||||
it('should use default_value from variable when available', () => {
|
||||
const variables = [
|
||||
makeVariable({ type: 'text-input', default_value: 'hello' }),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.test_var).toBe('hello')
|
||||
})
|
||||
|
||||
it('should prefer lastRunInputData over default_value', () => {
|
||||
const variables = [
|
||||
makeVariable({ type: 'text-input', default_value: 'default' }),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const lastRunInputData = { test_var: 'last-run-value' }
|
||||
const { result } = renderHook(() => useInitialData(variables, lastRunInputData))
|
||||
|
||||
expect(result.current.test_var).toBe('last-run-value')
|
||||
})
|
||||
|
||||
it('should handle multiple variables', () => {
|
||||
const variables = [
|
||||
makeVariable({ type: 'text-input', variable: 'name', default_value: 'Alice' }),
|
||||
makeVariable({ type: 'number', variable: 'age', default_value: 25 }),
|
||||
makeVariable({ type: 'checkbox', variable: 'agree' }),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useInitialData(variables))
|
||||
|
||||
expect(result.current.name).toBe('Alice')
|
||||
expect(result.current.age).toBe(25)
|
||||
expect(result.current.agree).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useConfigurations', () => {
|
||||
it('should convert variables to BaseConfiguration format', () => {
|
||||
const variables = [
|
||||
makeVariable({
|
||||
type: 'text-input',
|
||||
variable: 'name',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
max_length: 50,
|
||||
placeholder: 'Enter name',
|
||||
tooltips: 'Your full name',
|
||||
unit: '',
|
||||
}),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current).toHaveLength(1)
|
||||
expect(result.current[0]).toMatchObject({
|
||||
type: BaseFieldType.textInput,
|
||||
variable: 'name',
|
||||
label: 'Name',
|
||||
required: true,
|
||||
maxLength: 50,
|
||||
placeholder: 'Enter name',
|
||||
tooltip: 'Your full name',
|
||||
})
|
||||
})
|
||||
|
||||
it('should map select options correctly', () => {
|
||||
const variables = [
|
||||
makeVariable({
|
||||
type: 'select',
|
||||
variable: 'color',
|
||||
options: ['red', 'green', 'blue'],
|
||||
}),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current[0].options).toEqual([
|
||||
{ label: 'red', value: 'red' },
|
||||
{ label: 'green', value: 'green' },
|
||||
{ label: 'blue', value: 'blue' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle undefined options', () => {
|
||||
const variables = [
|
||||
makeVariable({ type: 'text-input' }),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current[0].options).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should include file-related fields for file type', () => {
|
||||
const variables = [
|
||||
makeVariable({
|
||||
type: 'file',
|
||||
variable: 'doc',
|
||||
allowed_file_types: ['pdf', 'docx'],
|
||||
allowed_file_extensions: ['.pdf', '.docx'],
|
||||
allowed_file_upload_methods: ['local', 'remote'],
|
||||
}),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current[0].allowedFileTypes).toEqual(['pdf', 'docx'])
|
||||
expect(result.current[0].allowedFileExtensions).toEqual(['.pdf', '.docx'])
|
||||
expect(result.current[0].allowedFileUploadMethods).toEqual(['local', 'remote'])
|
||||
})
|
||||
|
||||
it('should include showConditions as empty array', () => {
|
||||
const variables = [
|
||||
makeVariable(),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current[0].showConditions).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle multiple variables', () => {
|
||||
const variables = [
|
||||
makeVariable({ variable: 'a', type: 'text-input' }),
|
||||
makeVariable({ variable: 'b', type: 'number' }),
|
||||
makeVariable({ variable: 'c', type: 'checkbox' }),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current).toHaveLength(3)
|
||||
expect(result.current[0].variable).toBe('a')
|
||||
expect(result.current[1].variable).toBe('b')
|
||||
expect(result.current[2].variable).toBe('c')
|
||||
})
|
||||
|
||||
it('should include unit field', () => {
|
||||
const variables = [
|
||||
makeVariable({ type: 'number', unit: 'px' }),
|
||||
] as unknown as RAGPipelineVariables
|
||||
const { result } = renderHook(() => useConfigurations(variables))
|
||||
|
||||
expect(result.current[0].unit).toBe('px')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,68 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { usePipelineTemplate } from './use-pipeline-template'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
START_INITIAL_POSITION: { x: 100, y: 200 },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/knowledge-base/default', () => ({
|
||||
default: {
|
||||
metaData: { type: 'knowledge-base' },
|
||||
defaultValue: { title: 'Knowledge Base' },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
generateNewNode: ({ id, data, position }: { id: string, data: Record<string, unknown>, position: { x: number, y: number } }) => ({
|
||||
newNode: { id, data, position, type: 'custom' },
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('usePipelineTemplate', () => {
|
||||
it('should return nodes array with one knowledge base node', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
expect(result.current.nodes).toHaveLength(1)
|
||||
expect(result.current.nodes[0].id).toBe('knowledgeBase')
|
||||
})
|
||||
|
||||
it('should return empty edges array', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
expect(result.current.edges).toEqual([])
|
||||
})
|
||||
|
||||
it('should set node type from knowledge-base default', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
expect(result.current.nodes[0].data.type).toBe('knowledge-base')
|
||||
})
|
||||
|
||||
it('should set node as selected', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
expect(result.current.nodes[0].data.selected).toBe(true)
|
||||
})
|
||||
|
||||
it('should position node offset from START_INITIAL_POSITION', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
// x = 100 + 500 = 600, y = 200
|
||||
expect(result.current.nodes[0].position.x).toBe(600)
|
||||
expect(result.current.nodes[0].position.y).toBe(200)
|
||||
})
|
||||
|
||||
it('should translate node title', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
expect(result.current.nodes[0].data.title).toBe('blocks.knowledge-base')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user