test: add unit tests for rag-pipeline components to improve coverage and reliability

- Introduced new test files for RagPipelineChildren, PipelineScreenShot, QAItem, and various input field components.
- Enhanced testing for rendering, interactions, and state management across multiple components.
- Ensured comprehensive coverage for input field utilities and document processing functionalities.
This commit is contained in:
CodingOnStar
2026-04-08 10:29:50 +08:00
parent d863effb36
commit 1f4e127af5
25 changed files with 1650 additions and 0 deletions

View File

@ -0,0 +1,141 @@
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
import RagPipelineChildren from '../rag-pipeline-children'
let mockShowImportDSLModal = false
let mockSubscription: ((value: { type: string, payload?: { data?: EnvironmentVariable[] } }) => void) | null = null
const {
mockSetShowImportDSLModal,
mockHandlePaneContextmenuCancel,
mockExportCheck,
mockHandleExportDSL,
mockUseRagPipelineSearch,
} = vi.hoisted(() => ({
mockSetShowImportDSLModal: vi.fn((value: boolean) => {
mockShowImportDSLModal = value
}),
mockHandlePaneContextmenuCancel: vi.fn(),
mockExportCheck: vi.fn(),
mockHandleExportDSL: vi.fn(),
mockUseRagPipelineSearch: vi.fn(),
}))
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: {
useSubscription: (callback: (value: { type: string, payload?: { data?: EnvironmentVariable[] } }) => void) => {
mockSubscription = callback
},
},
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: {
showImportDSLModal: boolean
setShowImportDSLModal: typeof mockSetShowImportDSLModal
}) => unknown) => selector({
showImportDSLModal: mockShowImportDSLModal,
setShowImportDSLModal: mockSetShowImportDSLModal,
}),
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useDSL: () => ({
exportCheck: mockExportCheck,
handleExportDSL: mockHandleExportDSL,
}),
usePanelInteractions: () => ({
handlePaneContextmenuCancel: mockHandlePaneContextmenuCancel,
}),
}))
vi.mock('../../hooks/use-rag-pipeline-search', () => ({
useRagPipelineSearch: mockUseRagPipelineSearch,
}))
vi.mock('../../../workflow/plugin-dependency', () => ({
default: () => <div data-testid="plugin-dependency" />,
}))
vi.mock('../panel', () => ({
default: () => <div data-testid="rag-panel" />,
}))
vi.mock('../publish-toast', () => ({
default: () => <div data-testid="publish-toast" />,
}))
vi.mock('../rag-pipeline-header', () => ({
default: () => <div data-testid="rag-header" />,
}))
vi.mock('../update-dsl-modal', () => ({
default: ({ onCancel }: { onCancel: () => void }) => (
<div data-testid="update-dsl-modal">
<button onClick={onCancel}>close import</button>
</div>
),
}))
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
default: ({
envList,
onConfirm,
onClose,
}: {
envList: EnvironmentVariable[]
onConfirm: () => void
onClose: () => void
}) => (
<div data-testid="dsl-export-modal">
<div>{envList.map(env => env.name).join(',')}</div>
<button onClick={onConfirm}>confirm export</button>
<button onClick={onClose}>close export</button>
</div>
),
}))
describe('RagPipelineChildren', () => {
beforeEach(() => {
vi.clearAllMocks()
mockShowImportDSLModal = false
mockSubscription = null
})
it('should render the main pipeline children and the import modal when enabled', () => {
mockShowImportDSLModal = true
render(<RagPipelineChildren />)
fireEvent.click(screen.getByText('close import'))
expect(mockUseRagPipelineSearch).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('plugin-dependency')).toBeInTheDocument()
expect(screen.getByTestId('rag-header')).toBeInTheDocument()
expect(screen.getByTestId('rag-panel')).toBeInTheDocument()
expect(screen.getByTestId('publish-toast')).toBeInTheDocument()
expect(screen.getByTestId('update-dsl-modal')).toBeInTheDocument()
expect(mockSetShowImportDSLModal).toHaveBeenCalledWith(false)
})
it('should show the DSL export confirmation modal after receiving the export event', () => {
render(<RagPipelineChildren />)
act(() => {
mockSubscription?.({
type: DSL_EXPORT_CHECK,
payload: {
data: [{ name: 'API_KEY' } as EnvironmentVariable],
},
})
})
fireEvent.click(screen.getByText('confirm export'))
expect(screen.getByTestId('dsl-export-modal')).toHaveTextContent('API_KEY')
expect(mockHandleExportDSL).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,29 @@
import { render, screen } from '@testing-library/react'
import PipelineScreenShot from '../screenshot'
vi.mock('@/hooks/use-theme', () => ({
default: () => ({
theme: 'dark',
}),
}))
vi.mock('@/utils/var', () => ({
basePath: '/console',
}))
describe('PipelineScreenShot', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should build themed screenshot sources', () => {
const { container } = render(<PipelineScreenShot />)
const sources = container.querySelectorAll('source')
expect(sources).toHaveLength(3)
expect(sources[0]).toHaveAttribute('srcset', '/console/screenshots/dark/Pipeline.png')
expect(sources[1]).toHaveAttribute('srcset', '/console/screenshots/dark/Pipeline@2x.png')
expect(sources[2]).toHaveAttribute('srcset', '/console/screenshots/dark/Pipeline@3x.png')
expect(screen.getByAltText('Pipeline Screenshot')).toHaveAttribute('src', '/console/screenshots/dark/Pipeline.png')
})
})

View File

@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react'
import QAItem from '../q-a-item'
import { QAItemType } from '../types'
describe('QAItem', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the question prefix', () => {
render(<QAItem type={QAItemType.Question} text="What is Dify?" />)
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('What is Dify?')).toBeInTheDocument()
})
it('should render the answer prefix', () => {
render(<QAItem type={QAItemType.Answer} text="An LLM app platform." />)
expect(screen.getByText('A')).toBeInTheDocument()
expect(screen.getByText('An LLM app platform.')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,97 @@
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { VAR_ITEM_TEMPLATE_IN_PIPELINE } from '@/config'
import { PipelineInputVarType } from '@/models/pipeline'
import { TransferMethod } from '@/types/app'
import { convertFormDataToINputField, convertToInputFieldFormData } from '../utils'
describe('input-field editor utils', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should convert pipeline input vars into form data', () => {
const result = convertToInputFieldFormData({
type: PipelineInputVarType.multiFiles,
label: 'Upload files',
variable: 'documents',
max_length: 5,
default_value: 'initial-value',
required: false,
tooltips: 'Tooltip text',
options: ['a', 'b'],
placeholder: 'Select files',
unit: 'MB',
allowed_file_upload_methods: [TransferMethod.local_file],
allowed_file_types: [SupportUploadFileTypes.document],
allowed_file_extensions: ['pdf'],
})
expect(result).toEqual({
type: PipelineInputVarType.multiFiles,
label: 'Upload files',
variable: 'documents',
maxLength: 5,
default: 'initial-value',
required: false,
tooltips: 'Tooltip text',
options: ['a', 'b'],
placeholder: 'Select files',
unit: 'MB',
allowedFileUploadMethods: [TransferMethod.local_file],
allowedTypesAndExtensions: {
allowedFileTypes: [SupportUploadFileTypes.document],
allowedFileExtensions: ['pdf'],
},
})
})
it('should fall back to the default input variable template', () => {
const result = convertToInputFieldFormData()
expect(result).toEqual({
type: VAR_ITEM_TEMPLATE_IN_PIPELINE.type,
label: VAR_ITEM_TEMPLATE_IN_PIPELINE.label,
variable: VAR_ITEM_TEMPLATE_IN_PIPELINE.variable,
maxLength: undefined,
required: VAR_ITEM_TEMPLATE_IN_PIPELINE.required,
options: VAR_ITEM_TEMPLATE_IN_PIPELINE.options,
allowedTypesAndExtensions: {},
})
})
it('should convert form data back into pipeline input variables', () => {
const result = convertFormDataToINputField({
type: PipelineInputVarType.select,
label: 'Category',
variable: 'category',
maxLength: 10,
default: 'books',
required: true,
tooltips: 'Pick one',
options: ['books', 'music'],
placeholder: 'Choose',
unit: '',
allowedFileUploadMethods: [TransferMethod.local_file],
allowedTypesAndExtensions: {
allowedFileTypes: [SupportUploadFileTypes.document],
allowedFileExtensions: ['txt'],
},
})
expect(result).toEqual({
type: PipelineInputVarType.select,
label: 'Category',
variable: 'category',
max_length: 10,
default_value: 'books',
required: true,
tooltips: 'Pick one',
options: ['books', 'music'],
placeholder: 'Choose',
unit: '',
allowed_file_upload_methods: [TransferMethod.local_file],
allowed_file_types: [SupportUploadFileTypes.document],
allowed_file_extensions: ['txt'],
})
})
})

View File

@ -0,0 +1,79 @@
import type { ComponentType } from 'react'
import { useStore } from '@tanstack/react-form'
import { render, screen } from '@testing-library/react'
import HiddenFields from '../hidden-fields'
import { useHiddenConfigurations } from '../hooks'
type MockForm = {
store: object
}
const {
mockForm,
mockInputField,
} = vi.hoisted(() => ({
mockForm: {
store: {},
} as MockForm,
mockInputField: vi.fn(({ config }: { config: { variable: string } }) => {
return function FieldComponent() {
return <div data-testid="input-field">{config.variable}</div>
}
}),
}))
vi.mock('@tanstack/react-form', () => ({
useStore: vi.fn(),
}))
vi.mock('@/app/components/base/form', () => ({
withForm: ({ render }: {
render: (props: { form: MockForm }) => React.ReactNode
}) => ({ form }: { form?: MockForm }) => render({ form: form ?? mockForm }),
}))
vi.mock('@/app/components/base/form/form-scenarios/input-field/field', () => ({
default: mockInputField,
}))
vi.mock('../hooks', () => ({
useHiddenConfigurations: vi.fn(),
}))
describe('HiddenFields', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useStore).mockImplementation((_, selector) => selector({
values: {
options: ['option-a', 'option-b'],
},
}))
})
it('should build fields from the hidden configuration list', () => {
vi.mocked(useHiddenConfigurations).mockReturnValue([
{ variable: 'default' },
{ variable: 'tooltips' },
] as ReturnType<typeof useHiddenConfigurations>)
const HiddenFieldsComp = HiddenFields({ initialData: { variable: 'field_1' } }) as unknown as ComponentType
render(<HiddenFieldsComp />)
expect(useHiddenConfigurations).toHaveBeenCalledWith({
options: ['option-a', 'option-b'],
})
expect(mockInputField).toHaveBeenCalledTimes(2)
expect(screen.getAllByTestId('input-field')).toHaveLength(2)
expect(screen.getByText('default')).toBeInTheDocument()
expect(screen.getByText('tooltips')).toBeInTheDocument()
})
it('should render nothing when there are no hidden configurations', () => {
vi.mocked(useHiddenConfigurations).mockReturnValue([])
const HiddenFieldsComp = HiddenFields({}) as unknown as ComponentType
const { container } = render(<HiddenFieldsComp />)
expect(container).toBeEmptyDOMElement()
})
})

View File

@ -0,0 +1,85 @@
import type { ComponentType } from 'react'
import { render, screen } from '@testing-library/react'
import { useConfigurations } from '../hooks'
import InitialFields from '../initial-fields'
type MockForm = {
store: object
getFieldValue: (fieldName: string) => unknown
setFieldValue: (fieldName: string, value: unknown) => void
}
const {
mockForm,
mockInputField,
} = vi.hoisted(() => ({
mockForm: {
store: {},
getFieldValue: vi.fn(),
setFieldValue: vi.fn(),
} as MockForm,
mockInputField: vi.fn(({ config }: { config: { variable: string } }) => {
return function FieldComponent() {
return <div data-testid="input-field">{config.variable}</div>
}
}),
}))
vi.mock('@/app/components/base/form', () => ({
withForm: ({ render }: {
render: (props: { form: MockForm }) => React.ReactNode
}) => ({ form }: { form?: MockForm }) => render({ form: form ?? mockForm }),
}))
vi.mock('@/app/components/base/form/form-scenarios/input-field/field', () => ({
default: mockInputField,
}))
vi.mock('../hooks', () => ({
useConfigurations: vi.fn(),
}))
describe('InitialFields', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should build initial fields with the form accessors and supportFile flag', () => {
vi.mocked(useConfigurations).mockReturnValue([
{ variable: 'type' },
{ variable: 'label' },
] as ReturnType<typeof useConfigurations>)
const InitialFieldsComp = InitialFields({
initialData: { variable: 'field_1' },
supportFile: true,
}) as unknown as ComponentType
render(<InitialFieldsComp />)
expect(useConfigurations).toHaveBeenCalledWith(expect.objectContaining({
supportFile: true,
getFieldValue: expect.any(Function),
setFieldValue: expect.any(Function),
}))
expect(screen.getAllByTestId('input-field')).toHaveLength(2)
expect(screen.getByText('type')).toBeInTheDocument()
expect(screen.getByText('label')).toBeInTheDocument()
})
it('should delegate field accessors to the underlying form instance', () => {
vi.mocked(useConfigurations).mockReturnValue([] as ReturnType<typeof useConfigurations>)
mockForm.getFieldValue = vi.fn(() => 'label-value')
mockForm.setFieldValue = vi.fn()
const InitialFieldsComp = InitialFields({ supportFile: false }) as unknown as ComponentType
render(<InitialFieldsComp />)
const call = vi.mocked(useConfigurations).mock.calls[0]?.[0]
const value = call?.getFieldValue('label')
call?.setFieldValue('label', 'next-value')
expect(value).toBe('label-value')
expect(mockForm.getFieldValue).toHaveBeenCalledWith('label')
expect(mockForm.setFieldValue).toHaveBeenCalledWith('label', 'next-value')
})
})

View File

@ -0,0 +1,69 @@
import type { ComponentType } from 'react'
import { useStore } from '@tanstack/react-form'
import { fireEvent, render, screen } from '@testing-library/react'
import { PipelineInputVarType } from '@/models/pipeline'
import { useHiddenFieldNames } from '../hooks'
import ShowAllSettings from '../show-all-settings'
type MockForm = {
store: object
}
const mockForm = vi.hoisted(() => ({
store: {},
})) as MockForm
vi.mock('@tanstack/react-form', () => ({
useStore: vi.fn(),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/base/form', () => ({
withForm: ({ render }: {
render: (props: { form: MockForm }) => React.ReactNode
}) => ({ form }: { form?: MockForm }) => render({ form: form ?? mockForm }),
}))
vi.mock('../hooks', () => ({
useHiddenFieldNames: vi.fn(),
}))
describe('ShowAllSettings', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useStore).mockImplementation((_, selector) => selector({
values: {
type: PipelineInputVarType.textInput,
},
}))
vi.mocked(useHiddenFieldNames).mockReturnValue('default value, placeholder')
})
it('should render the summary and hidden field names', () => {
const ShowAllSettingsComp = ShowAllSettings({
handleShowAllSettings: vi.fn(),
}) as unknown as ComponentType
render(<ShowAllSettingsComp />)
expect(useHiddenFieldNames).toHaveBeenCalledWith(PipelineInputVarType.textInput)
expect(screen.getByText('appDebug.variableConfig.showAllSettings')).toBeInTheDocument()
expect(screen.getByText('default value, placeholder')).toBeInTheDocument()
})
it('should call the click handler when the row is pressed', () => {
const handleShowAllSettings = vi.fn()
const ShowAllSettingsComp = ShowAllSettings({
handleShowAllSettings,
}) as unknown as ComponentType
render(<ShowAllSettingsComp />)
fireEvent.click(screen.getByText('appDebug.variableConfig.showAllSettings'))
expect(handleShowAllSettings).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,89 @@
import type { InputVar } from '@/models/pipeline'
import { fireEvent, render, screen } from '@testing-library/react'
import { PipelineInputVarType } from '@/models/pipeline'
import FieldItem from '../field-item'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
const createInputVar = (overrides: Partial<InputVar> = {}): InputVar => ({
type: PipelineInputVarType.textInput,
label: 'Field Label',
variable: 'field_name',
max_length: 48,
default_value: '',
required: true,
tooltips: '',
options: [],
placeholder: '',
unit: '',
allowed_file_upload_methods: [],
allowed_file_types: [],
allowed_file_extensions: [],
...overrides,
})
describe('FieldItem', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the variable, label, and required badge', () => {
render(
<FieldItem
payload={createInputVar()}
index={0}
onClickEdit={vi.fn()}
onRemove={vi.fn()}
/>,
)
expect(screen.getByText('field_name')).toBeInTheDocument()
expect(screen.getByText('Field Label')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.start.required')).toBeInTheDocument()
})
it('should show edit and delete controls on hover and trigger both callbacks', () => {
const onClickEdit = vi.fn()
const onRemove = vi.fn()
const { container } = render(
<FieldItem
payload={createInputVar({ variable: 'custom_field' })}
index={2}
onClickEdit={onClickEdit}
onRemove={onRemove}
/>,
)
fireEvent.mouseEnter(container.firstChild!)
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0])
fireEvent.click(buttons[1])
expect(onClickEdit).toHaveBeenCalledWith('custom_field')
expect(onRemove).toHaveBeenCalledWith(2)
})
it('should keep the row readonly when readonly is enabled', () => {
const onClickEdit = vi.fn()
const onRemove = vi.fn()
const { container } = render(
<FieldItem
readonly
payload={createInputVar()}
index={0}
onClickEdit={onClickEdit}
onRemove={onRemove}
/>,
)
fireEvent.mouseEnter(container.firstChild!)
expect(screen.queryAllByRole('button')).toHaveLength(0)
expect(onClickEdit).not.toHaveBeenCalled()
expect(onRemove).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,91 @@
import type { InputVar } from '@/models/pipeline'
import { fireEvent, render, screen } from '@testing-library/react'
import { PipelineInputVarType } from '@/models/pipeline'
import FieldListContainer from '../field-list-container'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('react-sortablejs', () => ({
ReactSortable: ({
children,
list,
setList,
disabled,
}: {
children: React.ReactNode
list: Array<{ id: string }>
setList: (list: Array<{ id: string }>) => void
disabled?: boolean
}) => (
<div data-testid="sortable" data-disabled={String(disabled)}>
{children}
<button onClick={() => setList(list)}>same list</button>
<button onClick={() => setList([...list].reverse())}>reverse list</button>
</div>
),
}))
const createInputVar = (variable: string): InputVar => ({
type: PipelineInputVarType.textInput,
label: variable,
variable,
max_length: 48,
default_value: '',
required: true,
tooltips: '',
options: [],
placeholder: '',
unit: '',
allowed_file_upload_methods: [],
allowed_file_types: [],
allowed_file_extensions: [],
})
describe('FieldListContainer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the field items and ignore unchanged sort events', () => {
const onListSortChange = vi.fn()
render(
<FieldListContainer
inputFields={[createInputVar('field_1'), createInputVar('field_2')]}
onListSortChange={onListSortChange}
onRemoveField={vi.fn()}
onEditField={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('same list'))
expect(screen.getAllByText('field_1')).toHaveLength(2)
expect(screen.getAllByText('field_2')).toHaveLength(2)
expect(onListSortChange).not.toHaveBeenCalled()
})
it('should forward changed sort lists and honor readonly mode', () => {
const onListSortChange = vi.fn()
render(
<FieldListContainer
readonly
inputFields={[createInputVar('field_1'), createInputVar('field_2')]}
onListSortChange={onListSortChange}
onRemoveField={vi.fn()}
onEditField={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('reverse list'))
expect(screen.getByTestId('sortable')).toHaveAttribute('data-disabled', 'true')
expect(onListSortChange).toHaveBeenCalledWith([
expect.objectContaining({ id: 'field_2' }),
expect.objectContaining({ id: 'field_1' }),
])
})
})

View File

@ -0,0 +1,24 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { render, screen } from '@testing-library/react'
import Datasource from '../datasource'
vi.mock('@/app/components/workflow/hooks', () => ({
useToolIcon: () => 'tool-icon',
}))
vi.mock('@/app/components/workflow/block-icon', () => ({
default: ({ toolIcon }: { toolIcon: string }) => <div data-testid="block-icon">{toolIcon}</div>,
}))
describe('Datasource', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the datasource title and icon', () => {
render(<Datasource nodeData={{ title: 'Knowledge Base' } as DataSourceNodeType} />)
expect(screen.getByTestId('block-icon')).toHaveTextContent('tool-icon')
expect(screen.getByText('Knowledge Base')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,29 @@
import { render, screen } from '@testing-library/react'
import GlobalInputs from '../global-inputs'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/base/tooltip', () => ({
default: ({
popupContent,
}: {
popupContent: React.ReactNode
}) => <div data-testid="tooltip">{popupContent}</div>,
}))
describe('GlobalInputs', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the title and tooltip copy', () => {
render(<GlobalInputs />)
expect(screen.getByText('datasetPipeline.inputFieldPanel.globalInputs.title')).toBeInTheDocument()
expect(screen.getByTestId('tooltip')).toHaveTextContent('datasetPipeline.inputFieldPanel.globalInputs.tooltip')
})
})

View File

@ -0,0 +1,79 @@
import type { Datasource } from '../../../test-run/types'
import { fireEvent, render, screen } from '@testing-library/react'
import DataSource from '../data-source'
const {
mockOnSelect,
mockUseDraftPipelinePreProcessingParams,
} = vi.hoisted(() => ({
mockOnSelect: vi.fn(),
mockUseDraftPipelinePreProcessingParams: vi.fn(() => ({
data: {
variables: [{ variable: 'source' }],
},
})),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: { pipelineId: string }) => string) => selector({ pipelineId: 'pipeline-1' }),
}))
vi.mock('@/service/use-pipeline', () => ({
useDraftPipelinePreProcessingParams: mockUseDraftPipelinePreProcessingParams,
}))
vi.mock('../../../test-run/preparation/data-source-options', () => ({
default: ({
onSelect,
dataSourceNodeId,
}: {
onSelect: (data: Datasource) => void
dataSourceNodeId: string
}) => (
<div data-testid="data-source-options" data-node-id={dataSourceNodeId}>
<button
onClick={() => onSelect({ nodeId: 'source-node' } as Datasource)}
>
select datasource
</button>
</div>
),
}))
vi.mock('../form', () => ({
default: ({ variables }: { variables: Array<{ variable: string }> }) => (
<div data-testid="preview-form">{variables.map(item => item.variable).join(',')}</div>
),
}))
describe('DataSource preview', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the datasource selection step and forward selected values', () => {
render(
<DataSource
onSelect={mockOnSelect}
dataSourceNodeId="node-1"
/>,
)
fireEvent.click(screen.getByText('select datasource'))
expect(screen.getByText('datasetPipeline.inputFieldPanel.preview.stepOneTitle')).toBeInTheDocument()
expect(screen.getByTestId('data-source-options')).toHaveAttribute('data-node-id', 'node-1')
expect(screen.getByTestId('preview-form')).toHaveTextContent('source')
expect(mockUseDraftPipelinePreProcessingParams).toHaveBeenCalledWith({
pipeline_id: 'pipeline-1',
node_id: 'node-1',
}, true)
expect(mockOnSelect).toHaveBeenCalledWith({ nodeId: 'source-node' })
})
})

View File

@ -0,0 +1,64 @@
import type { RAGPipelineVariables } from '@/models/pipeline'
import { render, screen } from '@testing-library/react'
import Form from '../form'
type MockForm = {
id: string
}
const {
mockForm,
mockBaseField,
mockUseInitialData,
mockUseConfigurations,
} = vi.hoisted(() => ({
mockForm: {
id: 'form-1',
} as MockForm,
mockBaseField: vi.fn(({ config }: { config: { variable: string } }) => {
return function FieldComponent() {
return <div data-testid="base-field">{config.variable}</div>
}
}),
mockUseInitialData: vi.fn(() => ({ source: 'node-1' })),
mockUseConfigurations: vi.fn(() => [{ variable: 'source' }, { variable: 'chunkSize' }]),
}))
vi.mock('@/app/components/base/form', () => ({
useAppForm: () => mockForm,
}))
vi.mock('@/app/components/base/form/form-scenarios/base/field', () => ({
default: mockBaseField,
}))
vi.mock('@/app/components/rag-pipeline/hooks/use-input-fields', () => ({
useInitialData: mockUseInitialData,
useConfigurations: mockUseConfigurations,
}))
describe('Preview form', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should build fields from the pipeline variable configuration', () => {
render(<Form variables={[{ variable: 'source' }] as unknown as RAGPipelineVariables} />)
expect(mockUseInitialData).toHaveBeenCalled()
expect(mockUseConfigurations).toHaveBeenCalled()
expect(screen.getAllByTestId('base-field')).toHaveLength(2)
expect(screen.getByText('source')).toBeInTheDocument()
expect(screen.getByText('chunkSize')).toBeInTheDocument()
})
it('should prevent the native form submission', () => {
const { container } = render(<Form variables={[] as unknown as RAGPipelineVariables} />)
const form = container.querySelector('form')!
const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
form.dispatchEvent(submitEvent)
expect(submitEvent.defaultPrevented).toBe(true)
})
})

View File

@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react'
import ProcessDocuments from '../process-documents'
const mockUseDraftPipelineProcessingParams = vi.hoisted(() => vi.fn(() => ({
data: {
variables: [{ variable: 'chunkSize' }],
},
})))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: { pipelineId: string }) => string) => selector({ pipelineId: 'pipeline-1' }),
}))
vi.mock('@/service/use-pipeline', () => ({
useDraftPipelineProcessingParams: mockUseDraftPipelineProcessingParams,
}))
vi.mock('../form', () => ({
default: ({ variables }: { variables: Array<{ variable: string }> }) => (
<div data-testid="preview-form">{variables.map(item => item.variable).join(',')}</div>
),
}))
describe('ProcessDocuments preview', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the processing step and its variables', () => {
render(<ProcessDocuments dataSourceNodeId="node-2" />)
expect(screen.getByText('datasetPipeline.inputFieldPanel.preview.stepTwoTitle')).toBeInTheDocument()
expect(screen.getByTestId('preview-form')).toHaveTextContent('chunkSize')
expect(mockUseDraftPipelineProcessingParams).toHaveBeenCalledWith({
pipeline_id: 'pipeline-1',
node_id: 'node-2',
}, true)
})
})

View File

@ -0,0 +1,70 @@
import { fireEvent, render, screen } from '@testing-library/react'
import Header from '../header'
const {
mockSetIsPreparingDataSource,
mockHandleCancelDebugAndPreviewPanel,
mockWorkflowStore,
} = vi.hoisted(() => ({
mockSetIsPreparingDataSource: vi.fn(),
mockHandleCancelDebugAndPreviewPanel: vi.fn(),
mockWorkflowStore: {
getState: vi.fn(() => ({
isPreparingDataSource: true,
setIsPreparingDataSource: vi.fn(),
})),
},
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useWorkflowStore: () => mockWorkflowStore,
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useWorkflowInteractions: () => ({
handleCancelDebugAndPreviewPanel: mockHandleCancelDebugAndPreviewPanel,
}),
}))
vi.mock('@remixicon/react', () => ({
RiCloseLine: () => <span data-testid="close-icon" />,
}))
describe('TestRun header', () => {
beforeEach(() => {
vi.clearAllMocks()
mockWorkflowStore.getState.mockReturnValue({
isPreparingDataSource: true,
setIsPreparingDataSource: mockSetIsPreparingDataSource,
})
})
it('should render the title and reset preparing state on close', () => {
render(<Header />)
fireEvent.click(screen.getByTestId('close-icon').parentElement!)
expect(screen.getByText('datasetPipeline.testRun.title')).toBeInTheDocument()
expect(mockSetIsPreparingDataSource).toHaveBeenCalledWith(false)
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
})
it('should only cancel the panel when the datasource preparation flag is false', () => {
mockWorkflowStore.getState.mockReturnValue({
isPreparingDataSource: false,
setIsPreparingDataSource: mockSetIsPreparingDataSource,
})
render(<Header />)
fireEvent.click(screen.getByTestId('close-icon').parentElement!)
expect(mockSetIsPreparingDataSource).not.toHaveBeenCalled()
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react'
import FooterTips from '../footer-tips'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
describe('FooterTips', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the localized footer copy', () => {
render(<FooterTips />)
expect(screen.getByText('datasetPipeline.testRun.tooltip')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,41 @@
import { render, screen } from '@testing-library/react'
import StepIndicator from '../step-indicator'
describe('StepIndicator', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render all step labels and highlight the current step', () => {
const { container } = render(
<StepIndicator
currentStep={2}
steps={[
{ label: 'Select source', value: 'source' },
{ label: 'Process docs', value: 'process' },
{ label: 'Run test', value: 'run' },
]}
/>,
)
expect(screen.getByText('Select source')).toBeInTheDocument()
expect(screen.getByText('Process docs')).toBeInTheDocument()
expect(screen.getByText('Run test')).toBeInTheDocument()
expect(container.querySelector('.bg-state-accent-solid')).toBeInTheDocument()
expect(screen.getByText('Process docs').parentElement).toHaveClass('text-state-accent-solid')
})
it('should keep inactive steps in the tertiary state', () => {
render(
<StepIndicator
currentStep={1}
steps={[
{ label: 'Select source', value: 'source' },
{ label: 'Process docs', value: 'process' },
]}
/>,
)
expect(screen.getByText('Process docs').parentElement).toHaveClass('text-text-tertiary')
})
})

View File

@ -0,0 +1,49 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { fireEvent, render, screen } from '@testing-library/react'
import OptionCard from '../option-card'
vi.mock('@/app/components/workflow/hooks', () => ({
useToolIcon: () => 'source-icon',
}))
vi.mock('@/app/components/workflow/block-icon', () => ({
default: ({ toolIcon }: { toolIcon: string }) => <div data-testid="block-icon">{toolIcon}</div>,
}))
describe('OptionCard', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render the datasource label and icon', () => {
render(
<OptionCard
label="Website Crawl"
value="website"
selected={false}
nodeData={{ title: 'Website Crawl' } as DataSourceNodeType}
/>,
)
expect(screen.getByTestId('block-icon')).toHaveTextContent('source-icon')
expect(screen.getByText('Website Crawl')).toBeInTheDocument()
})
it('should call onClick with the card value and apply selected styles', () => {
const onClick = vi.fn()
render(
<OptionCard
label="Online Drive"
value="online-drive"
selected
nodeData={{ title: 'Online Drive' } as DataSourceNodeType}
onClick={onClick}
/>,
)
fireEvent.click(screen.getByText('Online Drive'))
expect(onClick).toHaveBeenCalledWith('online-drive')
expect(screen.getByText('Online Drive')).toHaveClass('text-text-primary')
})
})

View File

@ -0,0 +1,73 @@
import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions'
import { fireEvent, render, screen } from '@testing-library/react'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import Actions from '../actions'
let mockWorkflowRunningData: { result: { status: WorkflowRunningStatus } } | undefined
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: { workflowRunningData: typeof mockWorkflowRunningData }) => unknown) => selector({
workflowRunningData: mockWorkflowRunningData,
}),
}))
const createFormParams = (overrides: Partial<CustomActionsProps> = {}): CustomActionsProps => ({
form: {
handleSubmit: vi.fn(),
} as unknown as CustomActionsProps['form'],
isSubmitting: false,
canSubmit: true,
...overrides,
})
describe('Document processing actions', () => {
beforeEach(() => {
vi.clearAllMocks()
mockWorkflowRunningData = undefined
})
it('should render back/process actions and trigger both callbacks', () => {
const onBack = vi.fn()
const formParams = createFormParams()
render(<Actions formParams={formParams} onBack={onBack} />)
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.backToDataSource' }))
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.process' }))
expect(onBack).toHaveBeenCalledTimes(1)
expect(formParams.form.handleSubmit).toHaveBeenCalledTimes(1)
})
it('should disable processing when runDisabled or the workflow is already running', () => {
const { rerender } = render(
<Actions
formParams={createFormParams()}
onBack={vi.fn()}
runDisabled
/>,
)
expect(screen.getByRole('button', { name: 'datasetPipeline.operations.process' })).toBeDisabled()
mockWorkflowRunningData = {
result: {
status: WorkflowRunningStatus.Running,
},
}
rerender(
<Actions
formParams={createFormParams()}
onBack={vi.fn()}
/>,
)
expect(screen.getByRole('button', { name: /datasetPipeline\.operations\.process/i })).toBeDisabled()
})
})

View File

@ -0,0 +1,32 @@
import { renderHook } from '@testing-library/react'
import { useInputVariables } from '../hooks'
const mockUseDraftPipelineProcessingParams = vi.hoisted(() => vi.fn(() => ({
data: { variables: [{ variable: 'chunkSize' }] },
isFetching: true,
})))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: { pipelineId: string }) => string) => selector({ pipelineId: 'pipeline-1' }),
}))
vi.mock('@/service/use-pipeline', () => ({
useDraftPipelineProcessingParams: mockUseDraftPipelineProcessingParams,
}))
describe('useInputVariables', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should query processing params with the current pipeline id and datasource node id', () => {
const { result } = renderHook(() => useInputVariables('datasource-node'))
expect(mockUseDraftPipelineProcessingParams).toHaveBeenCalledWith({
pipeline_id: 'pipeline-1',
node_id: 'datasource-node',
})
expect(result.current.isFetchingParams).toBe(true)
expect(result.current.paramsConfig).toEqual({ variables: [{ variable: 'chunkSize' }] })
})
})

View File

@ -0,0 +1,140 @@
import type { ZodSchema } from 'zod'
import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions'
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import Options from '../options'
const {
mockFormValue,
mockHandleSubmit,
mockToastError,
mockBaseField,
} = vi.hoisted(() => ({
mockFormValue: { chunkSize: 256 } as Record<string, unknown>,
mockHandleSubmit: vi.fn(),
mockToastError: vi.fn(),
mockBaseField: vi.fn(({ config }: { config: { variable: string } }) => {
return function FieldComponent() {
return <div data-testid="base-field">{config.variable}</div>
}
}),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: {
error: mockToastError,
},
}))
vi.mock('@/app/components/base/form/form-scenarios/base/field', () => ({
default: mockBaseField,
}))
vi.mock('@/app/components/base/form', () => ({
useAppForm: ({
onSubmit,
validators,
}: {
onSubmit: (params: { value: Record<string, unknown> }) => void
validators?: {
onSubmit?: (params: { value: Record<string, unknown> }) => string | undefined
}
}) => ({
handleSubmit: () => {
const validationResult = validators?.onSubmit?.({ value: mockFormValue })
if (!validationResult)
onSubmit({ value: mockFormValue })
mockHandleSubmit()
},
AppForm: ({ children }: { children: React.ReactNode }) => <div data-testid="app-form">{children}</div>,
Actions: ({ CustomActions }: { CustomActions: (props: CustomActionsProps) => React.ReactNode }) => (
<div data-testid="form-actions">
{CustomActions({
form: {
handleSubmit: mockHandleSubmit,
} as unknown as CustomActionsProps['form'],
isSubmitting: false,
canSubmit: true,
})}
</div>
),
}),
}))
const createSchema = (success: boolean): ZodSchema => ({
safeParse: vi.fn(() => {
if (success)
return { success: true }
return {
success: false,
error: {
issues: [{
path: ['chunkSize'],
message: 'Invalid value',
}],
},
}
}),
}) as unknown as ZodSchema
describe('Document processing options', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render base fields and the custom actions slot', () => {
render(
<Options
initialData={{ chunkSize: 100 }}
configurations={[{ variable: 'chunkSize' } as BaseConfiguration]}
schema={createSchema(true)}
CustomActions={() => <div data-testid="custom-actions">custom actions</div>}
onSubmit={vi.fn()}
/>,
)
expect(screen.getByTestId('base-field')).toHaveTextContent('chunkSize')
expect(screen.getByTestId('form-actions')).toBeInTheDocument()
expect(screen.getByTestId('custom-actions')).toBeInTheDocument()
})
it('should validate and toast the first schema error before submitting', async () => {
const onSubmit = vi.fn()
const { container } = render(
<Options
initialData={{ chunkSize: 100 }}
configurations={[]}
schema={createSchema(false)}
CustomActions={() => <div>actions</div>}
onSubmit={onSubmit}
/>,
)
fireEvent.submit(container.querySelector('form')!)
await waitFor(() => {
expect(mockToastError).toHaveBeenCalledWith('Path: chunkSize Error: Invalid value')
})
expect(onSubmit).not.toHaveBeenCalled()
})
it('should submit the parsed form value when validation succeeds', async () => {
const onSubmit = vi.fn()
const { container } = render(
<Options
initialData={{ chunkSize: 100 }}
configurations={[]}
schema={createSchema(true)}
CustomActions={() => <div>actions</div>}
onSubmit={onSubmit}
/>,
)
fireEvent.submit(container.querySelector('form')!)
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(mockFormValue)
})
})
})

View File

@ -0,0 +1,84 @@
import { ChunkingMode } from '@/models/datasets'
import { formatPreviewChunks } from '../utils'
vi.mock('@/config', () => ({
RAG_PIPELINE_PREVIEW_CHUNK_NUM: 2,
}))
describe('result preview utils', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should return undefined for empty outputs', () => {
expect(formatPreviewChunks(undefined)).toBeUndefined()
expect(formatPreviewChunks(null)).toBeUndefined()
})
it('should format text chunks and limit them to the preview length', () => {
const result = formatPreviewChunks({
chunk_structure: ChunkingMode.text,
preview: [
{ content: 'Chunk 1', summary: 'S1' },
{ content: 'Chunk 2', summary: 'S2' },
{ content: 'Chunk 3', summary: 'S3' },
],
})
expect(result).toEqual([
{ content: 'Chunk 1', summary: 'S1' },
{ content: 'Chunk 2', summary: 'S2' },
])
})
it('should format paragraph and full-doc parent-child previews differently', () => {
const paragraph = formatPreviewChunks({
chunk_structure: ChunkingMode.parentChild,
parent_mode: 'paragraph',
preview: [
{ content: 'Parent 1', child_chunks: ['c1', 'c2', 'c3'] },
{ content: 'Parent 2', child_chunks: ['c4'] },
{ content: 'Parent 3', child_chunks: ['c5'] },
],
})
const fullDoc = formatPreviewChunks({
chunk_structure: ChunkingMode.parentChild,
parent_mode: 'full-doc',
preview: [
{ content: 'Parent 1', child_chunks: ['c1', 'c2', 'c3'] },
],
})
expect(paragraph).toEqual({
parent_mode: 'paragraph',
parent_child_chunks: [
{ parent_content: 'Parent 1', parent_summary: undefined, child_contents: ['c1', 'c2', 'c3'], parent_mode: 'paragraph' },
{ parent_content: 'Parent 2', parent_summary: undefined, child_contents: ['c4'], parent_mode: 'paragraph' },
],
})
expect(fullDoc).toEqual({
parent_mode: 'full-doc',
parent_child_chunks: [
{ parent_content: 'Parent 1', child_contents: ['c1', 'c2'], parent_mode: 'full-doc' },
],
})
})
it('should format qa previews and limit them to the preview size', () => {
const result = formatPreviewChunks({
chunk_structure: ChunkingMode.qa,
qa_preview: [
{ question: 'Q1', answer: 'A1' },
{ question: 'Q2', answer: 'A2' },
{ question: 'Q3', answer: 'A3' },
],
})
expect(result).toEqual({
qa_chunks: [
{ question: 'Q1', answer: 'A1' },
{ question: 'Q2', answer: 'A2' },
],
})
})
})

View File

@ -0,0 +1,64 @@
import type { WorkflowRunningData } from '@/app/components/workflow/types'
import { fireEvent, render, screen } from '@testing-library/react'
import Tab from '../tab'
const createWorkflowRunningData = (): WorkflowRunningData => ({
task_id: 'task-1',
message_id: 'message-1',
conversation_id: 'conversation-1',
result: {
workflow_id: 'workflow-1',
inputs: '{}',
inputs_truncated: false,
process_data: '{}',
process_data_truncated: false,
outputs: '{}',
outputs_truncated: false,
status: 'succeeded',
elapsed_time: 10,
total_tokens: 20,
created_at: Date.now(),
finished_at: Date.now(),
steps: 1,
total_steps: 1,
},
tracing: [],
})
describe('Tab', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render an active tab and pass its value on click', () => {
const onClick = vi.fn()
render(
<Tab
isActive
label="Preview"
value="preview"
workflowRunningData={createWorkflowRunningData()}
onClick={onClick}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'Preview' }))
expect(screen.getByRole('button')).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
expect(onClick).toHaveBeenCalledWith('preview')
})
it('should disable the tab when workflow run data is unavailable', () => {
render(
<Tab
isActive={false}
label="Trace"
value="trace"
onClick={vi.fn()}
/>,
)
expect(screen.getByRole('button', { name: 'Trace' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'Trace' })).toHaveClass('opacity-30')
})
})

View File

@ -0,0 +1,41 @@
import { fireEvent, render, screen } from '@testing-library/react'
import InputFieldButton from '../input-field-button'
const {
mockSetShowInputFieldPanel,
mockSetShowEnvPanel,
} = vi.hoisted(() => ({
mockSetShowInputFieldPanel: vi.fn(),
mockSetShowEnvPanel: vi.fn(),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
}),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: {
setShowInputFieldPanel: typeof mockSetShowInputFieldPanel
setShowEnvPanel: typeof mockSetShowEnvPanel
}) => unknown) => selector({
setShowInputFieldPanel: mockSetShowInputFieldPanel,
setShowEnvPanel: mockSetShowEnvPanel,
}),
}))
describe('InputFieldButton', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should open the input field panel and close the env panel', () => {
render(<InputFieldButton />)
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.inputField' }))
expect(mockSetShowInputFieldPanel).toHaveBeenCalledWith(true)
expect(mockSetShowEnvPanel).toHaveBeenCalledWith(false)
})
})

View File

@ -0,0 +1,92 @@
import type { Viewport } from 'reactflow'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { processNodesWithoutDataSource } from '../nodes'
vi.mock('@/app/components/workflow/constants', () => ({
CUSTOM_NODE: 'custom',
NODE_WIDTH_X_OFFSET: 400,
START_INITIAL_POSITION: { x: 100, y: 100 },
}))
vi.mock('@/app/components/workflow/nodes/data-source-empty/constants', () => ({
CUSTOM_DATA_SOURCE_EMPTY_NODE: 'data-source-empty',
}))
vi.mock('@/app/components/workflow/note-node/constants', () => ({
CUSTOM_NOTE_NODE: 'note',
}))
vi.mock('@/app/components/workflow/note-node/types', () => ({
NoteTheme: { blue: 'blue' },
}))
vi.mock('@/app/components/workflow/utils', () => ({
generateNewNode: ({ id, type, data, position }: { id: string, type: string, data: object, position: { x: number, y: number } }) => ({
newNode: { id, type, data, position },
}),
}))
describe('processNodesWithoutDataSource', () => {
it('should return the original nodes when a datasource node already exists', () => {
const nodes = [
{
id: 'node-1',
type: 'custom',
data: { type: BlockEnum.DataSource },
position: { x: 100, y: 100 },
},
] as Node[]
const viewport: Viewport = { x: 0, y: 0, zoom: 1 }
const result = processNodesWithoutDataSource(nodes, viewport)
expect(result.nodes).toBe(nodes)
expect(result.viewport).toBe(viewport)
})
it('should prepend datasource empty and note nodes when the pipeline starts without a datasource', () => {
const nodes = [
{
id: 'node-1',
type: 'custom',
data: { type: BlockEnum.KnowledgeBase },
position: { x: 300, y: 200 },
},
] as Node[]
const result = processNodesWithoutDataSource(nodes, { x: 0, y: 0, zoom: 2 })
expect(result.nodes[0]).toEqual(expect.objectContaining({
id: 'data-source-empty',
type: 'data-source-empty',
position: { x: -100, y: 200 },
}))
expect(result.nodes[1]).toEqual(expect.objectContaining({
id: 'note',
type: 'note',
position: { x: -100, y: 300 },
}))
expect(result.viewport).toEqual({
x: 400,
y: -200,
zoom: 2,
})
})
it('should leave nodes unchanged when there is no custom node to anchor from', () => {
const nodes = [
{
id: 'node-1',
type: 'note',
data: { type: BlockEnum.Answer },
position: { x: 100, y: 100 },
},
] as Node[]
const result = processNodesWithoutDataSource(nodes)
expect(result.nodes).toBe(nodes)
expect(result.viewport).toBeUndefined()
})
})