test: add integration tests for DSL export/import flow, input field CRUD, and test run flow

This commit is contained in:
CodingOnStar
2026-02-11 15:11:48 +08:00
parent faefb98746
commit cebf141525
15 changed files with 2595 additions and 1 deletions

View 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',
}))
})
})
})

View 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')
})
})
})

View File

@ -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', () => {

View 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([])
})
})
})

View File

@ -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')
})
})
})

View 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',
}))
})
})

View File

@ -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',
)
})
})

View File

@ -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()
})
})
})

View File

@ -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)
})
})
})

View File

@ -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)
})
})

View File

@ -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 })
})
})

View File

@ -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)
})
})

View File

@ -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)
})
})
})

View 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')
})
})

View File

@ -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')
})
})