mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox
# Conflicts: # api/uv.lock # web/app/components/apps/__tests__/app-card.spec.tsx # web/app/components/apps/__tests__/list.spec.tsx # web/app/components/datasets/create/__tests__/index.spec.tsx # web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx # web/app/components/plugins/readme-panel/__tests__/index.spec.tsx # web/app/components/rag-pipeline/__tests__/index.spec.tsx # web/app/components/rag-pipeline/hooks/__tests__/index.spec.ts # web/eslint-suppressions.json
This commit is contained in:
@ -0,0 +1,69 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Actions from '../actions'
|
||||
|
||||
describe('Actions', () => {
|
||||
const defaultProps = {
|
||||
onBack: vi.fn(),
|
||||
onProcess: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering: verify both action buttons render with correct labels
|
||||
describe('Rendering', () => {
|
||||
it('should render back button and process button', () => {
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons).toHaveLength(2)
|
||||
expect(screen.getByText('datasetPipeline.operations.dataSource')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.operations.saveAndProcess')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User interactions: clicking back and process buttons
|
||||
describe('User Interactions', () => {
|
||||
it('should call onBack when back button clicked', () => {
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.dataSource'))
|
||||
|
||||
expect(defaultProps.onBack).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onProcess when process button clicked', () => {
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.saveAndProcess'))
|
||||
|
||||
expect(defaultProps.onProcess).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
// Props: disabled state for the process button
|
||||
describe('Props', () => {
|
||||
it('should disable process button when runDisabled is true', () => {
|
||||
render(<Actions {...defaultProps} runDisabled />)
|
||||
|
||||
const processButton = screen.getByText('datasetPipeline.operations.saveAndProcess').closest('button')
|
||||
expect(processButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable process button when runDisabled is false', () => {
|
||||
render(<Actions {...defaultProps} runDisabled={false} />)
|
||||
|
||||
const processButton = screen.getByText('datasetPipeline.operations.saveAndProcess').closest('button')
|
||||
expect(processButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable process button when runDisabled is undefined', () => {
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
const processButton = screen.getByText('datasetPipeline.operations.saveAndProcess').closest('button')
|
||||
expect(processButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -4,18 +4,14 @@ import * as React from 'react'
|
||||
import * as z from 'zod'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Actions from './actions'
|
||||
import Form from './form'
|
||||
import Header from './header'
|
||||
import Actions from '../actions'
|
||||
import Form from '../form'
|
||||
import Header from '../header'
|
||||
|
||||
// ==========================================
|
||||
// Spy on Toast.notify for validation tests
|
||||
// ==========================================
|
||||
const toastNotifySpy = vi.spyOn(Toast, 'notify')
|
||||
|
||||
// ==========================================
|
||||
// Test Data Factory Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Creates mock configuration for testing
|
||||
@ -56,9 +52,7 @@ const createFailingSchema = () => {
|
||||
} as unknown as z.ZodType
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Actions Component Tests
|
||||
// ==========================================
|
||||
describe('Actions', () => {
|
||||
const defaultActionsProps = {
|
||||
onBack: vi.fn(),
|
||||
@ -69,137 +63,101 @@ describe('Actions', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultActionsProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.operations.dataSource')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.operations.saveAndProcess')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render back button with arrow icon', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultActionsProps} />)
|
||||
|
||||
// Assert
|
||||
const backButton = screen.getByRole('button', { name: /datasetPipeline.operations.dataSource/i })
|
||||
expect(backButton).toBeInTheDocument()
|
||||
expect(backButton.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render process button', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultActionsProps} />)
|
||||
|
||||
// Assert
|
||||
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
|
||||
expect(processButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct container layout', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Actions {...defaultActionsProps} />)
|
||||
|
||||
// Assert
|
||||
const mainContainer = container.querySelector('.flex.items-center.justify-between')
|
||||
expect(mainContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('runDisabled prop', () => {
|
||||
it('should not disable process button when runDisabled is false', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultActionsProps} runDisabled={false} />)
|
||||
|
||||
// Assert
|
||||
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
|
||||
expect(processButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable process button when runDisabled is true', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultActionsProps} runDisabled={true} />)
|
||||
|
||||
// Assert
|
||||
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
|
||||
expect(processButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not disable process button when runDisabled is undefined', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultActionsProps} runDisabled={undefined} />)
|
||||
|
||||
// Assert
|
||||
const processButton = screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i })
|
||||
expect(processButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions Testing
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onBack when back button is clicked', () => {
|
||||
// Arrange
|
||||
const onBack = vi.fn()
|
||||
render(<Actions {...defaultActionsProps} onBack={onBack} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.dataSource/i }))
|
||||
|
||||
// Assert
|
||||
expect(onBack).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onProcess when process button is clicked', () => {
|
||||
// Arrange
|
||||
const onProcess = vi.fn()
|
||||
render(<Actions {...defaultActionsProps} onProcess={onProcess} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i }))
|
||||
|
||||
// Assert
|
||||
expect(onProcess).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onProcess when process button is disabled and clicked', () => {
|
||||
// Arrange
|
||||
const onProcess = vi.fn()
|
||||
render(<Actions {...defaultActionsProps} onProcess={onProcess} runDisabled={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.operations.saveAndProcess/i }))
|
||||
|
||||
// Assert
|
||||
expect(onProcess).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Component Memoization Testing
|
||||
// ==========================================
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert
|
||||
expect(Actions.$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Header Component Tests
|
||||
// ==========================================
|
||||
describe('Header', () => {
|
||||
const defaultHeaderProps = {
|
||||
onReset: vi.fn(),
|
||||
@ -211,73 +169,53 @@ describe('Header', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render reset button', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /common.operation.reset/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render preview button with icon', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(previewButton).toBeInTheDocument()
|
||||
expect(previewButton.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render title with correct text', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct container layout', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Header {...defaultHeaderProps} />)
|
||||
|
||||
// Assert
|
||||
const mainContainer = container.querySelector('.flex.items-center.gap-x-1')
|
||||
expect(mainContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('resetDisabled prop', () => {
|
||||
it('should not disable reset button when resetDisabled is false', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} resetDisabled={false} />)
|
||||
|
||||
// Assert
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
expect(resetButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable reset button when resetDisabled is true', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} resetDisabled={true} />)
|
||||
|
||||
// Assert
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
expect(resetButton).toBeDisabled()
|
||||
})
|
||||
@ -285,32 +223,25 @@ describe('Header', () => {
|
||||
|
||||
describe('previewDisabled prop', () => {
|
||||
it('should not disable preview button when previewDisabled is false', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} previewDisabled={false} />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(previewButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable preview button when previewDisabled is true', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} previewDisabled={true} />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(previewButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle onPreview being undefined', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} onPreview={undefined} />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(previewButton).toBeInTheDocument()
|
||||
// Click should not throw
|
||||
let didThrow = false
|
||||
try {
|
||||
fireEvent.click(previewButton)
|
||||
@ -322,78 +253,57 @@ describe('Header', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions Testing
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onReset when reset button is clicked', () => {
|
||||
// Arrange
|
||||
const onReset = vi.fn()
|
||||
render(<Header {...defaultHeaderProps} onReset={onReset} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /common.operation.reset/i }))
|
||||
|
||||
// Assert
|
||||
expect(onReset).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onReset when reset button is disabled and clicked', () => {
|
||||
// Arrange
|
||||
const onReset = vi.fn()
|
||||
render(<Header {...defaultHeaderProps} onReset={onReset} resetDisabled={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /common.operation.reset/i }))
|
||||
|
||||
// Assert
|
||||
expect(onReset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
// Arrange
|
||||
const onPreview = vi.fn()
|
||||
render(<Header {...defaultHeaderProps} onPreview={onPreview} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i }))
|
||||
|
||||
// Assert
|
||||
expect(onPreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onPreview when preview button is disabled and clicked', () => {
|
||||
// Arrange
|
||||
const onPreview = vi.fn()
|
||||
render(<Header {...defaultHeaderProps} onPreview={onPreview} previewDisabled={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i }))
|
||||
|
||||
// Assert
|
||||
expect(onPreview).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Component Memoization Testing
|
||||
// ==========================================
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert
|
||||
expect(Header.$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases Testing
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle both buttons disabled', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} resetDisabled={true} previewDisabled={true} />)
|
||||
|
||||
// Assert
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(resetButton).toBeDisabled()
|
||||
@ -401,10 +311,8 @@ describe('Header', () => {
|
||||
})
|
||||
|
||||
it('should handle both buttons enabled', () => {
|
||||
// Arrange & Act
|
||||
render(<Header {...defaultHeaderProps} resetDisabled={false} previewDisabled={false} />)
|
||||
|
||||
// Assert
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(resetButton).not.toBeDisabled()
|
||||
@ -413,9 +321,7 @@ describe('Header', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Form Component Tests
|
||||
// ==========================================
|
||||
describe('Form', () => {
|
||||
const defaultFormProps = {
|
||||
initialData: { field1: '' },
|
||||
@ -432,66 +338,48 @@ describe('Form', () => {
|
||||
toastNotifySpy.mockClear()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render form element', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Form {...defaultFormProps} />)
|
||||
|
||||
// Assert
|
||||
const form = container.querySelector('form')
|
||||
expect(form).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Header component', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common.operation.reset/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct form structure', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Form {...defaultFormProps} />)
|
||||
|
||||
// Assert
|
||||
const form = container.querySelector('form.flex.w-full.flex-col')
|
||||
expect(form).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('isRunning prop', () => {
|
||||
it('should disable preview button when isRunning is true', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} isRunning={true} />)
|
||||
|
||||
// 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 & Act
|
||||
render(<Form {...defaultFormProps} isRunning={false} />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(previewButton).not.toBeDisabled()
|
||||
})
|
||||
@ -499,7 +387,6 @@ describe('Form', () => {
|
||||
|
||||
describe('configurations prop', () => {
|
||||
it('should render empty when configurations is empty', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Form {...defaultFormProps} configurations={[]} />)
|
||||
|
||||
// Assert - the fields container should have no field children
|
||||
@ -508,17 +395,14 @@ describe('Form', () => {
|
||||
})
|
||||
|
||||
it('should render all configurations', () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'var1', label: 'Variable 1' }),
|
||||
createMockConfiguration({ variable: 'var2', label: 'Variable 2' }),
|
||||
createMockConfiguration({ variable: 'var3', label: 'Variable 3' }),
|
||||
]
|
||||
|
||||
// Act
|
||||
render(<Form {...defaultFormProps} configurations={configurations} initialData={{ var1: '', var2: '', var3: '' }} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Variable 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Variable 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Variable 3')).toBeInTheDocument()
|
||||
@ -526,24 +410,18 @@ describe('Form', () => {
|
||||
})
|
||||
|
||||
it('should expose submit method via ref', () => {
|
||||
// Arrange
|
||||
const mockRef = { current: null } as React.MutableRefObject<{ submit: () => void } | null>
|
||||
|
||||
// Act
|
||||
render(<Form {...defaultFormProps} ref={mockRef} />)
|
||||
|
||||
// Assert
|
||||
expect(mockRef.current).not.toBeNull()
|
||||
expect(typeof mockRef.current?.submit).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Ref Submit Testing
|
||||
// ==========================================
|
||||
describe('Ref Submit', () => {
|
||||
it('should call onSubmit when ref.submit() is called', async () => {
|
||||
// Arrange
|
||||
const onSubmit = vi.fn()
|
||||
const mockRef = { current: null } as React.MutableRefObject<{ submit: () => void } | null>
|
||||
render(<Form {...defaultFormProps} ref={mockRef} onSubmit={onSubmit} />)
|
||||
@ -551,14 +429,12 @@ describe('Form', () => {
|
||||
// Act - call submit via ref
|
||||
mockRef.current?.submit()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should trigger form validation when ref.submit() is called', async () => {
|
||||
// Arrange
|
||||
const failingSchema = createFailingSchema()
|
||||
const mockRef = { current: null } as React.MutableRefObject<{ submit: () => void } | null>
|
||||
render(<Form {...defaultFormProps} ref={mockRef} schema={failingSchema} />)
|
||||
@ -576,53 +452,40 @@ describe('Form', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions Testing
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
// Arrange
|
||||
const onPreview = vi.fn()
|
||||
render(<Form {...defaultFormProps} onPreview={onPreview} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i }))
|
||||
|
||||
// Assert
|
||||
expect(onPreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle form submission via form element', async () => {
|
||||
// Arrange
|
||||
const onSubmit = vi.fn()
|
||||
const { container } = render(<Form {...defaultFormProps} onSubmit={onSubmit} />)
|
||||
const form = container.querySelector('form')!
|
||||
|
||||
// Act
|
||||
fireEvent.submit(form)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Form State Testing
|
||||
// ==========================================
|
||||
describe('Form State', () => {
|
||||
it('should disable reset button initially when form is not dirty', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} />)
|
||||
|
||||
// Assert
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
expect(resetButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable reset button when form becomes dirty', async () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Field 1' }),
|
||||
]
|
||||
@ -633,7 +496,6 @@ describe('Form', () => {
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'new value' } })
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
expect(resetButton).not.toBeDisabled()
|
||||
@ -641,7 +503,6 @@ describe('Form', () => {
|
||||
})
|
||||
|
||||
it('should reset form to initial values when reset button is clicked', async () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Field 1' }),
|
||||
]
|
||||
@ -659,7 +520,6 @@ describe('Form', () => {
|
||||
expect(resetButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Click reset button
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
fireEvent.click(resetButton)
|
||||
|
||||
@ -670,7 +530,6 @@ describe('Form', () => {
|
||||
})
|
||||
|
||||
it('should call form.reset when handleReset is triggered', async () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Field 1' }),
|
||||
]
|
||||
@ -697,20 +556,15 @@ describe('Form', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Validation Testing
|
||||
// ==========================================
|
||||
describe('Validation', () => {
|
||||
it('should show toast notification on validation error', async () => {
|
||||
// Arrange
|
||||
const failingSchema = createFailingSchema()
|
||||
const { container } = render(<Form {...defaultFormProps} schema={failingSchema} />)
|
||||
|
||||
// Act
|
||||
const form = container.querySelector('form')!
|
||||
fireEvent.submit(form)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -720,12 +574,10 @@ describe('Form', () => {
|
||||
})
|
||||
|
||||
it('should not call onSubmit when validation fails', async () => {
|
||||
// Arrange
|
||||
const onSubmit = vi.fn()
|
||||
const failingSchema = createFailingSchema()
|
||||
const { container } = render(<Form {...defaultFormProps} schema={failingSchema} onSubmit={onSubmit} />)
|
||||
|
||||
// Act
|
||||
const form = container.querySelector('form')!
|
||||
fireEvent.submit(form)
|
||||
|
||||
@ -737,93 +589,71 @@ describe('Form', () => {
|
||||
})
|
||||
|
||||
it('should call onSubmit when validation passes', async () => {
|
||||
// Arrange
|
||||
const onSubmit = vi.fn()
|
||||
const passingSchema = createMockSchema()
|
||||
const { container } = render(<Form {...defaultFormProps} schema={passingSchema} onSubmit={onSubmit} />)
|
||||
|
||||
// Act
|
||||
const form = container.querySelector('form')!
|
||||
fireEvent.submit(form)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases Testing
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty initialData', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} initialData={{}} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle configurations with different field types', () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ type: BaseFieldType.textInput, variable: 'text', label: 'Text Field' }),
|
||||
createMockConfiguration({ type: BaseFieldType.numberInput, variable: 'number', label: 'Number Field' }),
|
||||
]
|
||||
|
||||
// Act
|
||||
render(<Form {...defaultFormProps} configurations={configurations} initialData={{ text: '', number: 0 }} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Text Field')).toBeInTheDocument()
|
||||
expect(screen.getByText('Number Field')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle null ref', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} ref={{ current: null }} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Configuration Variations Testing
|
||||
// ==========================================
|
||||
describe('Configuration Variations', () => {
|
||||
it('should render configuration with label', () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Custom Label' }),
|
||||
]
|
||||
|
||||
// Act
|
||||
render(<Form {...defaultFormProps} configurations={configurations} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Custom Label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render required configuration', () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Required Field', required: true }),
|
||||
]
|
||||
|
||||
// Act
|
||||
render(<Form {...defaultFormProps} configurations={configurations} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Required Field')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Integration Tests (Cross-component)
|
||||
// ==========================================
|
||||
describe('Process Documents Components Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -841,19 +671,15 @@ describe('Process Documents Components Integration', () => {
|
||||
}
|
||||
|
||||
it('should render Header within Form', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common.operation.reset/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass isRunning to Header for previewDisabled', () => {
|
||||
// Arrange & Act
|
||||
render(<Form {...defaultFormProps} isRunning={true} />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen.getByRole('button', { name: /datasetPipeline.addDocuments.stepTwo.previewChunks/i })
|
||||
expect(previewButton).toBeDisabled()
|
||||
})
|
||||
@ -0,0 +1,167 @@
|
||||
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
import Form from '../form'
|
||||
|
||||
// Mock the Header component (sibling component, not a base component)
|
||||
vi.mock('../header', () => ({
|
||||
default: ({ onReset, resetDisabled, onPreview, previewDisabled }: {
|
||||
onReset: () => void
|
||||
resetDisabled: boolean
|
||||
onPreview: () => void
|
||||
previewDisabled: boolean
|
||||
}) => (
|
||||
<div data-testid="form-header">
|
||||
<button data-testid="reset-btn" onClick={onReset} disabled={resetDisabled}>Reset</button>
|
||||
<button data-testid="preview-btn" onClick={onPreview} disabled={previewDisabled}>Preview</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
value: z.string().optional(),
|
||||
})
|
||||
|
||||
const defaultConfigs: BaseConfiguration[] = [
|
||||
{ variable: 'name', type: 'text-input', label: 'Name', required: true, showConditions: [] } as BaseConfiguration,
|
||||
{ variable: 'value', type: 'text-input', label: 'Value', required: false, showConditions: [] } as BaseConfiguration,
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
initialData: { name: 'test', value: '' },
|
||||
configurations: defaultConfigs,
|
||||
schema,
|
||||
onSubmit: vi.fn(),
|
||||
onPreview: vi.fn(),
|
||||
ref: { current: null },
|
||||
isRunning: false,
|
||||
}
|
||||
|
||||
describe('Form (process-documents)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
// Verify basic rendering of form structure
|
||||
describe('Rendering', () => {
|
||||
it('should render form with header and fields', () => {
|
||||
render(<Form {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('form-header')).toBeInTheDocument()
|
||||
expect(screen.getByText('Name')).toBeInTheDocument()
|
||||
expect(screen.getByText('Value')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all configuration fields', () => {
|
||||
const configs: BaseConfiguration[] = [
|
||||
{ variable: 'a', type: 'text-input', label: 'A', required: false, showConditions: [] } as BaseConfiguration,
|
||||
{ variable: 'b', type: 'text-input', label: 'B', required: false, showConditions: [] } as BaseConfiguration,
|
||||
{ variable: 'c', type: 'text-input', label: 'C', required: false, showConditions: [] } as BaseConfiguration,
|
||||
]
|
||||
|
||||
render(<Form {...defaultProps} configurations={configs} initialData={{ a: '', b: '', c: '' }} />)
|
||||
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
expect(screen.getByText('B')).toBeInTheDocument()
|
||||
expect(screen.getByText('C')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Verify form submission behavior
|
||||
describe('Form Submission', () => {
|
||||
it('should call onSubmit with valid data on form submit', async () => {
|
||||
render(<Form {...defaultProps} />)
|
||||
const form = screen.getByTestId('form-header').closest('form')!
|
||||
|
||||
fireEvent.submit(form)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onSubmit with valid data via imperative handle', async () => {
|
||||
const ref = { current: null as { submit: () => void } | null }
|
||||
render(<Form {...defaultProps} ref={ref} />)
|
||||
|
||||
ref.current?.submit()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Verify validation shows Toast on error
|
||||
describe('Validation', () => {
|
||||
it('should show toast error when validation fails', async () => {
|
||||
render(<Form {...defaultProps} initialData={{ name: '', value: '' }} />)
|
||||
const form = screen.getByTestId('form-header').closest('form')!
|
||||
|
||||
fireEvent.submit(form)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(Toast.notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not show toast error when validation passes', async () => {
|
||||
render(<Form {...defaultProps} />)
|
||||
const form = screen.getByTestId('form-header').closest('form')!
|
||||
|
||||
fireEvent.submit(form)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.onSubmit).toHaveBeenCalled()
|
||||
})
|
||||
expect(Toast.notify).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Verify header button states
|
||||
describe('Header Controls', () => {
|
||||
it('should pass isRunning to previewDisabled', () => {
|
||||
render(<Form {...defaultProps} isRunning={true} />)
|
||||
|
||||
expect(screen.getByTestId('preview-btn')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
render(<Form {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('preview-btn'))
|
||||
|
||||
expect(defaultProps.onPreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render reset button (disabled when form is not dirty)', () => {
|
||||
render(<Form {...defaultProps} />)
|
||||
|
||||
// Reset button is rendered but disabled since form is not dirty initially
|
||||
expect(screen.getByTestId('reset-btn')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('reset-btn')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// Verify edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should render with empty configurations array', () => {
|
||||
render(<Form {...defaultProps} configurations={[]} />)
|
||||
|
||||
expect(screen.getByTestId('form-header')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with empty initialData', () => {
|
||||
render(<Form {...defaultProps} initialData={{}} configurations={[]} />)
|
||||
|
||||
expect(screen.getByTestId('form-header')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,57 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Header from '../header'
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, disabled, variant }: { children: React.ReactNode, onClick: () => void, disabled?: boolean, variant: string }) => (
|
||||
<button data-testid={`btn-${variant}`} onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Header', () => {
|
||||
const defaultProps = {
|
||||
onReset: vi.fn(),
|
||||
resetDisabled: false,
|
||||
previewDisabled: false,
|
||||
onPreview: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render chunk settings title', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render reset and preview buttons', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByTestId('btn-ghost')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('btn-secondary-accent')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onReset when reset clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('btn-ghost'))
|
||||
expect(defaultProps.onReset).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('btn-secondary-accent'))
|
||||
expect(defaultProps.onPreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable reset button when resetDisabled is true', () => {
|
||||
render(<Header {...defaultProps} resetDisabled={true} />)
|
||||
expect(screen.getByTestId('btn-ghost')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable preview button when previewDisabled is true', () => {
|
||||
render(<Header {...defaultProps} previewDisabled={true} />)
|
||||
expect(screen.getByTestId('btn-secondary-accent')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,52 @@
|
||||
import type { PipelineProcessingParamsRequest } from '@/models/pipeline'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useInputVariables } from '../hooks'
|
||||
|
||||
const mockUseDatasetDetailContextWithSelector = vi.fn()
|
||||
const mockUsePublishedPipelineProcessingParams = vi.fn()
|
||||
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (selector: (value: unknown) => unknown) => mockUseDatasetDetailContextWithSelector(selector),
|
||||
}))
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
usePublishedPipelineProcessingParams: (params: PipelineProcessingParamsRequest) => mockUsePublishedPipelineProcessingParams(params),
|
||||
}))
|
||||
|
||||
describe('useInputVariables', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseDatasetDetailContextWithSelector.mockReturnValue('pipeline-123')
|
||||
mockUsePublishedPipelineProcessingParams.mockReturnValue({
|
||||
data: { inputs: [{ name: 'query', type: 'string' }] },
|
||||
isFetching: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return paramsConfig and isFetchingParams', () => {
|
||||
const { result } = renderHook(() => useInputVariables('node-1'))
|
||||
|
||||
expect(result.current.paramsConfig).toEqual({ inputs: [{ name: 'query', type: 'string' }] })
|
||||
expect(result.current.isFetchingParams).toBe(false)
|
||||
})
|
||||
|
||||
it('should call usePublishedPipelineProcessingParams with pipeline_id and node_id', () => {
|
||||
renderHook(() => useInputVariables('node-1'))
|
||||
|
||||
expect(mockUsePublishedPipelineProcessingParams).toHaveBeenCalledWith({
|
||||
pipeline_id: 'pipeline-123',
|
||||
node_id: 'node-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return isFetchingParams true when loading', () => {
|
||||
mockUsePublishedPipelineProcessingParams.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useInputVariables('node-1'))
|
||||
expect(result.current.isFetchingParams).toBe(true)
|
||||
expect(result.current.paramsConfig).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -3,17 +3,13 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
|
||||
import { useInputVariables } from './hooks'
|
||||
import ProcessDocuments from './index'
|
||||
|
||||
// ==========================================
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
import { useInputVariables } from '../hooks'
|
||||
import ProcessDocuments from '../index'
|
||||
|
||||
// Mock useInputVariables hook
|
||||
let mockIsFetchingParams = false
|
||||
let mockParamsConfig: { variables: unknown[] } | undefined = { variables: [] }
|
||||
vi.mock('./hooks', () => ({
|
||||
vi.mock('../hooks', () => ({
|
||||
useInputVariables: vi.fn(() => ({
|
||||
isFetchingParams: mockIsFetchingParams,
|
||||
paramsConfig: mockParamsConfig,
|
||||
@ -30,9 +26,7 @@ vi.mock('@/app/components/rag-pipeline/hooks/use-input-fields', () => ({
|
||||
useConfigurations: vi.fn(() => mockConfigurations),
|
||||
}))
|
||||
|
||||
// ==========================================
|
||||
// Test Data Factory Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Creates mock configuration for testing
|
||||
@ -64,10 +58,6 @@ const createDefaultProps = (overrides: Partial<React.ComponentProps<typeof Proce
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Test Suite
|
||||
// ==========================================
|
||||
|
||||
describe('ProcessDocuments', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -78,16 +68,11 @@ describe('ProcessDocuments', () => {
|
||||
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
|
||||
@ -95,10 +80,8 @@ describe('ProcessDocuments', () => {
|
||||
})
|
||||
|
||||
it('should render Form and Actions components', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert - check for elements from both components
|
||||
@ -108,80 +91,59 @@ describe('ProcessDocuments', () => {
|
||||
})
|
||||
|
||||
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 props = createDefaultProps({ dataSourceNodeId: 'custom-node-id' })
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(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()
|
||||
})
|
||||
@ -189,200 +151,153 @@ describe('ProcessDocuments', () => {
|
||||
|
||||
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 = vi.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 = vi.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 = vi.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 = vi.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 props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(useInitialData)).toHaveBeenCalledWith(mockVariables)
|
||||
})
|
||||
|
||||
it('should pass variables from useInputVariables to useConfigurations', () => {
|
||||
// Arrange
|
||||
const mockVariables = [{ variable: 'testVar', type: 'text', label: 'Test' }]
|
||||
mockParamsConfig = { variables: mockVariables }
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(useConfigurations)).toHaveBeenCalledWith(mockVariables)
|
||||
})
|
||||
|
||||
it('should use empty array when paramsConfig.variables is undefined', () => {
|
||||
// Arrange
|
||||
mockParamsConfig = { variables: undefined as unknown as unknown[] }
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(useInitialData)).toHaveBeenCalledWith([])
|
||||
expect(vi.mocked(useConfigurations)).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('should use empty array when paramsConfig is undefined', () => {
|
||||
// Arrange
|
||||
mockParamsConfig = undefined
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(useInitialData)).toHaveBeenCalledWith([])
|
||||
expect(vi.mocked(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
|
||||
@ -390,86 +305,65 @@ describe('ProcessDocuments', () => {
|
||||
})
|
||||
|
||||
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 props = createDefaultProps({ dataSourceNodeId: 'node-1' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<ProcessDocuments {...props} />)
|
||||
expect(vi.mocked(useInputVariables)).toHaveBeenLastCalledWith('node-1')
|
||||
|
||||
rerender(<ProcessDocuments {...props} dataSourceNodeId="node-2" />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(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 props = createDefaultProps({ dataSourceNodeId: 'node-id-with-special_chars:123' })
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(useInputVariables)).toHaveBeenCalledWith('node-id-with-special_chars:123')
|
||||
})
|
||||
|
||||
it('should handle long dataSourceNodeId', () => {
|
||||
// Arrange
|
||||
const longId = 'a'.repeat(1000)
|
||||
const props = createDefaultProps({ dataSourceNodeId: longId })
|
||||
|
||||
// Act
|
||||
render(<ProcessDocuments {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(vi.mocked(useInputVariables)).toHaveBeenCalledWith(longId)
|
||||
})
|
||||
|
||||
it('should handle multiple callbacks without interference', () => {
|
||||
// Arrange
|
||||
const onProcess = vi.fn()
|
||||
const onBack = vi.fn()
|
||||
const onPreview = vi.fn()
|
||||
@ -477,21 +371,17 @@ describe('ProcessDocuments', () => {
|
||||
|
||||
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 },
|
||||
@ -503,14 +393,11 @@ describe('ProcessDocuments', () => {
|
||||
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()
|
||||
@ -520,12 +407,9 @@ describe('ProcessDocuments', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// 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' }),
|
||||
@ -533,16 +417,13 @@ describe('ProcessDocuments', () => {
|
||||
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' }),
|
||||
@ -550,21 +431,16 @@ describe('ProcessDocuments', () => {
|
||||
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' }] }
|
||||
@ -581,10 +457,8 @@ describe('ProcessDocuments', () => {
|
||||
onBack: vi.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()
|
||||
Reference in New Issue
Block a user