Files
dify/web/app/components/datasets/documents/create-from-pipeline/process-documents/index.spec.tsx

602 lines
20 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import ProcessDocuments from './index'
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
// ==========================================
// Mock External Dependencies
// ==========================================
// Mock useInputVariables hook
let mockIsFetchingParams = false
let mockParamsConfig: { variables: unknown[] } | undefined = { variables: [] }
jest.mock('./hooks', () => ({
useInputVariables: jest.fn(() => ({
isFetchingParams: mockIsFetchingParams,
paramsConfig: mockParamsConfig,
})),
}))
// Mock useConfigurations hook
let mockConfigurations: BaseConfiguration[] = []
// Mock useInitialData hook
let mockInitialData: Record<string, unknown> = {}
jest.mock('@/app/components/rag-pipeline/hooks/use-input-fields', () => ({
useInitialData: jest.fn(() => mockInitialData),
useConfigurations: jest.fn(() => mockConfigurations),
}))
// ==========================================
// Test Data Factory Functions
// ==========================================
/**
* Creates mock configuration for testing
*/
const createMockConfiguration = (overrides: Partial<BaseConfiguration> = {}): BaseConfiguration => ({
type: BaseFieldType.textInput,
variable: 'testVariable',
label: 'Test Label',
required: false,
maxLength: undefined,
options: undefined,
showConditions: [],
placeholder: '',
tooltip: '',
...overrides,
})
/**
* Creates default test props
*/
const createDefaultProps = (overrides: Partial<React.ComponentProps<typeof ProcessDocuments>> = {}) => ({
dataSourceNodeId: 'test-node-id',
ref: { current: null } as React.RefObject<unknown>,
isRunning: false,
onProcess: jest.fn(),
onPreview: jest.fn(),
onSubmit: jest.fn(),
onBack: jest.fn(),
...overrides,
})
// ==========================================
// Test Suite
// ==========================================
describe('ProcessDocuments', () => {
beforeEach(() => {
jest.clearAllMocks()
// Reset mock values
mockIsFetchingParams = false
mockParamsConfig = { variables: [] }
mockInitialData = {}
mockConfigurations = []
})
// ==========================================
// Rendering Tests
// ==========================================
describe('Rendering', () => {
// Tests basic rendering functionality
it('should render without crashing', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert - check for Header title from Form component
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
})
it('should render Form and Actions components', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert - check for elements from both components
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.operations.dataSource')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.operations.saveAndProcess')).toBeInTheDocument()
})
it('should render with correct container structure', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<ProcessDocuments {...props} />)
// Assert
const mainContainer = container.querySelector('.flex.flex-col.gap-y-4.pt-4')
expect(mainContainer).toBeInTheDocument()
})
})
// ==========================================
// Props Testing
// ==========================================
describe('Props', () => {
describe('dataSourceNodeId prop', () => {
it('should pass dataSourceNodeId to useInputVariables hook', () => {
// Arrange
const { useInputVariables } = require('./hooks')
const props = createDefaultProps({ dataSourceNodeId: 'custom-node-id' })
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useInputVariables).toHaveBeenCalledWith('custom-node-id')
})
it('should handle empty dataSourceNodeId', () => {
// Arrange
const props = createDefaultProps({ dataSourceNodeId: '' })
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
})
})
describe('isRunning prop', () => {
it('should disable preview button when isRunning is true', () => {
// Arrange
const props = createDefaultProps({ isRunning: true })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
expect(previewButton).toBeDisabled()
})
it('should not disable preview button when isRunning is false', () => {
// Arrange
const props = createDefaultProps({ isRunning: false })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
expect(previewButton).not.toBeDisabled()
})
it('should disable process button in Actions when isRunning is true', () => {
// Arrange
mockIsFetchingParams = false
const props = createDefaultProps({ isRunning: true })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
expect(processButton).toBeDisabled()
})
})
describe('ref prop', () => {
it('should expose submit method via ref', () => {
// Arrange
const mockRef = { current: null } as React.MutableRefObject<{ submit: () => void } | null>
const props = createDefaultProps({ ref: mockRef })
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(mockRef.current).not.toBeNull()
expect(typeof mockRef.current?.submit).toBe('function')
})
})
})
// ==========================================
// User Interactions Testing
// ==========================================
describe('User Interactions', () => {
it('should call onProcess when Actions process button is clicked', () => {
// Arrange
const onProcess = jest.fn()
const props = createDefaultProps({ onProcess })
render(<ProcessDocuments {...props} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i }))
// Assert
expect(onProcess).toHaveBeenCalledTimes(1)
})
it('should call onBack when Actions back button is clicked', () => {
// Arrange
const onBack = jest.fn()
const props = createDefaultProps({ onBack })
render(<ProcessDocuments {...props} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.dataSource/i }))
// Assert
expect(onBack).toHaveBeenCalledTimes(1)
})
it('should call onPreview when preview button is clicked', () => {
// Arrange
const onPreview = jest.fn()
const props = createDefaultProps({ onPreview })
render(<ProcessDocuments {...props} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i }))
// Assert
expect(onPreview).toHaveBeenCalledTimes(1)
})
it('should call onSubmit when form is submitted', async () => {
// Arrange
const onSubmit = jest.fn()
const props = createDefaultProps({ onSubmit })
const { container } = render(<ProcessDocuments {...props} />)
// Act
const form = container.querySelector('form')!
fireEvent.submit(form)
// Assert
await waitFor(() => {
expect(onSubmit).toHaveBeenCalled()
})
})
})
// ==========================================
// Hook Integration Tests
// ==========================================
describe('Hook Integration', () => {
it('should pass variables from useInputVariables to useInitialData', () => {
// Arrange
const mockVariables = [{ variable: 'testVar', type: 'text', label: 'Test' }]
mockParamsConfig = { variables: mockVariables }
const { useInitialData } = require('@/app/components/rag-pipeline/hooks/use-input-fields')
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useInitialData).toHaveBeenCalledWith(mockVariables)
})
it('should pass variables from useInputVariables to useConfigurations', () => {
// Arrange
const mockVariables = [{ variable: 'testVar', type: 'text', label: 'Test' }]
mockParamsConfig = { variables: mockVariables }
const { useConfigurations } = require('@/app/components/rag-pipeline/hooks/use-input-fields')
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useConfigurations).toHaveBeenCalledWith(mockVariables)
})
it('should use empty array when paramsConfig.variables is undefined', () => {
// Arrange
mockParamsConfig = { variables: undefined as unknown as unknown[] }
const { useInitialData, useConfigurations } = require('@/app/components/rag-pipeline/hooks/use-input-fields')
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useInitialData).toHaveBeenCalledWith([])
expect(useConfigurations).toHaveBeenCalledWith([])
})
it('should use empty array when paramsConfig is undefined', () => {
// Arrange
mockParamsConfig = undefined
const { useInitialData, useConfigurations } = require('@/app/components/rag-pipeline/hooks/use-input-fields')
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useInitialData).toHaveBeenCalledWith([])
expect(useConfigurations).toHaveBeenCalledWith([])
})
})
// ==========================================
// Actions runDisabled Testing
// ==========================================
describe('Actions runDisabled', () => {
it('should disable process button when isFetchingParams is true', () => {
// Arrange
mockIsFetchingParams = true
const props = createDefaultProps({ isRunning: false })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
expect(processButton).toBeDisabled()
})
it('should disable process button when isRunning is true', () => {
// Arrange
mockIsFetchingParams = false
const props = createDefaultProps({ isRunning: true })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
expect(processButton).toBeDisabled()
})
it('should enable process button when both isFetchingParams and isRunning are false', () => {
// Arrange
mockIsFetchingParams = false
const props = createDefaultProps({ isRunning: false })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
expect(processButton).not.toBeDisabled()
})
it('should disable process button when both isFetchingParams and isRunning are true', () => {
// Arrange
mockIsFetchingParams = true
const props = createDefaultProps({ isRunning: true })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
expect(processButton).toBeDisabled()
})
})
// ==========================================
// Component Memoization Testing
// ==========================================
describe('Component Memoization', () => {
it('should be wrapped with React.memo', () => {
// Assert - verify component has memo wrapper
expect(ProcessDocuments.$$typeof).toBe(Symbol.for('react.memo'))
})
it('should render correctly after rerender with same props', () => {
// Arrange
const props = createDefaultProps()
// Act
const { rerender } = render(<ProcessDocuments {...props} />)
rerender(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
})
it('should update when dataSourceNodeId prop changes', () => {
// Arrange
const { useInputVariables } = require('./hooks')
const props = createDefaultProps({ dataSourceNodeId: 'node-1' })
// Act
const { rerender } = render(<ProcessDocuments {...props} />)
expect(useInputVariables).toHaveBeenLastCalledWith('node-1')
rerender(<ProcessDocuments {...props} dataSourceNodeId="node-2" />)
// Assert
expect(useInputVariables).toHaveBeenLastCalledWith('node-2')
})
})
// ==========================================
// Edge Cases Testing
// ==========================================
describe('Edge Cases', () => {
it('should handle undefined paramsConfig gracefully', () => {
// Arrange
mockParamsConfig = undefined
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
})
it('should handle empty variables array', () => {
// Arrange
mockParamsConfig = { variables: [] }
mockConfigurations = []
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
})
it('should handle special characters in dataSourceNodeId', () => {
// Arrange
const { useInputVariables } = require('./hooks')
const props = createDefaultProps({ dataSourceNodeId: 'node-id-with-special_chars:123' })
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useInputVariables).toHaveBeenCalledWith('node-id-with-special_chars:123')
})
it('should handle long dataSourceNodeId', () => {
// Arrange
const { useInputVariables } = require('./hooks')
const longId = 'a'.repeat(1000)
const props = createDefaultProps({ dataSourceNodeId: longId })
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(useInputVariables).toHaveBeenCalledWith(longId)
})
it('should handle multiple callbacks without interference', () => {
// Arrange
const onProcess = jest.fn()
const onBack = jest.fn()
const onPreview = jest.fn()
const props = createDefaultProps({ onProcess, onBack, onPreview })
render(<ProcessDocuments {...props} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i }))
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.dataSource/i }))
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i }))
// Assert
expect(onProcess).toHaveBeenCalledTimes(1)
expect(onBack).toHaveBeenCalledTimes(1)
expect(onPreview).toHaveBeenCalledTimes(1)
})
})
// ==========================================
// runDisabled Logic Testing (with test.each)
// ==========================================
describe('runDisabled Logic', () => {
const runDisabledTestCases = [
{ isFetchingParams: false, isRunning: false, expectedDisabled: false },
{ isFetchingParams: false, isRunning: true, expectedDisabled: true },
{ isFetchingParams: true, isRunning: false, expectedDisabled: true },
{ isFetchingParams: true, isRunning: true, expectedDisabled: true },
]
it.each(runDisabledTestCases)(
'should set process button disabled=$expectedDisabled when isFetchingParams=$isFetchingParams and isRunning=$isRunning',
({ isFetchingParams, isRunning, expectedDisabled }) => {
// Arrange
mockIsFetchingParams = isFetchingParams
const props = createDefaultProps({ isRunning })
// Act
render(<ProcessDocuments {...props} />)
// Assert
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
if (expectedDisabled)
expect(processButton).toBeDisabled()
else
expect(processButton).not.toBeDisabled()
},
)
})
// ==========================================
// Configuration Rendering Tests
// ==========================================
describe('Configuration Rendering', () => {
it('should render configurations as form fields', () => {
// Arrange
mockConfigurations = [
createMockConfiguration({ variable: 'var1', label: 'Variable 1' }),
createMockConfiguration({ variable: 'var2', label: 'Variable 2' }),
]
mockInitialData = { var1: '', var2: '' }
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('Variable 1')).toBeInTheDocument()
expect(screen.getByText('Variable 2')).toBeInTheDocument()
})
it('should handle configurations with different field types', () => {
// Arrange
mockConfigurations = [
createMockConfiguration({ type: BaseFieldType.textInput, variable: 'textVar', label: 'Text Field' }),
createMockConfiguration({ type: BaseFieldType.numberInput, variable: 'numberVar', label: 'Number Field' }),
]
mockInitialData = { textVar: '', numberVar: 0 }
const props = createDefaultProps()
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('Text Field')).toBeInTheDocument()
expect(screen.getByText('Number Field')).toBeInTheDocument()
})
})
// ==========================================
// Full Integration Props Testing
// ==========================================
describe('Full Prop Integration', () => {
it('should render correctly with all props provided', () => {
// Arrange
const mockRef = { current: null } as React.MutableRefObject<{ submit: () => void } | null>
mockIsFetchingParams = false
mockParamsConfig = { variables: [{ variable: 'testVar', type: 'text', label: 'Test' }] }
mockInitialData = { testVar: 'initial value' }
mockConfigurations = [createMockConfiguration({ variable: 'testVar', label: 'Test Variable' })]
const props = {
dataSourceNodeId: 'full-test-node',
ref: mockRef,
isRunning: false,
onProcess: jest.fn(),
onPreview: jest.fn(),
onSubmit: jest.fn(),
onBack: jest.fn(),
}
// Act
render(<ProcessDocuments {...props} />)
// Assert
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.operations.dataSource')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.operations.saveAndProcess')).toBeInTheDocument()
expect(screen.getByText('Test Variable')).toBeInTheDocument()
expect(mockRef.current).not.toBeNull()
})
})
})