feat(tests): add comprehensive tests for Processing and EmbeddingProcess components (#29873)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,825 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Actions from './index'
|
||||
|
||||
// ==========================================
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock next/navigation - useParams returns datasetId
|
||||
const mockDatasetId = 'test-dataset-id'
|
||||
jest.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: mockDatasetId }),
|
||||
}))
|
||||
|
||||
// Mock next/link to capture href
|
||||
jest.mock('next/link', () => {
|
||||
return ({ children, href, replace }: { children: React.ReactNode; href: string; replace?: boolean }) => (
|
||||
<a href={href} data-replace={replace}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Test Suite
|
||||
// ==========================================
|
||||
|
||||
describe('Actions', () => {
|
||||
// Default mock for required props
|
||||
const defaultProps = {
|
||||
handleNextStep: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
// Tests basic rendering functionality
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render cancel button with correct link', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
const cancelLink = screen.getByRole('link')
|
||||
expect(cancelLink).toHaveAttribute('href', `/datasets/${mockDatasetId}/documents`)
|
||||
expect(cancelLink).toHaveAttribute('data-replace', 'true')
|
||||
})
|
||||
|
||||
it('should render next step button with arrow icon', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })
|
||||
expect(nextButton).toBeInTheDocument()
|
||||
expect(nextButton.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render cancel button with correct translation key', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render select all section by default', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
// Tests for prop variations and defaults
|
||||
describe('disabled prop', () => {
|
||||
it('should not disable next step button when disabled is false', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} disabled={false} />)
|
||||
|
||||
// Assert
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })
|
||||
expect(nextButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable next step button when disabled is true', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} disabled={true} />)
|
||||
|
||||
// Assert
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })
|
||||
expect(nextButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not disable next step button when disabled is undefined', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} disabled={undefined} />)
|
||||
|
||||
// Assert
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })
|
||||
expect(nextButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('showSelect prop', () => {
|
||||
it('should show select all section when showSelect is true', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} showSelect={true} onSelectAll={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.operation.selectAll')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide select all section when showSelect is false', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} showSelect={false} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide select all section when showSelect defaults to false', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions handleNextStep={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('tip prop', () => {
|
||||
it('should show tip when showSelect is true and tip is provided', () => {
|
||||
// Arrange
|
||||
const tip = 'This is a helpful tip'
|
||||
|
||||
// Act
|
||||
render(<Actions {...defaultProps} showSelect={true} tip={tip} onSelectAll={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(tip)).toBeInTheDocument()
|
||||
expect(screen.getByTitle(tip)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show tip when showSelect is false even if tip is provided', () => {
|
||||
// Arrange
|
||||
const tip = 'This is a helpful tip'
|
||||
|
||||
// Act
|
||||
render(<Actions {...defaultProps} showSelect={false} tip={tip} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText(tip)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show tip when tip is empty string', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} showSelect={true} tip="" onSelectAll={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
const tipElements = screen.queryAllByTitle('')
|
||||
// Empty tip should not render a tip element
|
||||
expect(tipElements.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should use empty string as default tip value', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} showSelect={true} onSelectAll={jest.fn()} />)
|
||||
|
||||
// Assert - tip container should not exist when tip defaults to empty string
|
||||
const tipContainer = document.querySelector('.text-text-tertiary.truncate')
|
||||
expect(tipContainer).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Event Handlers Testing
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
// Tests for event handlers
|
||||
it('should call handleNextStep when next button is clicked', () => {
|
||||
// Arrange
|
||||
const handleNextStep = jest.fn()
|
||||
render(<Actions {...defaultProps} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call handleNextStep when next button is disabled and clicked', () => {
|
||||
// Arrange
|
||||
const handleNextStep = jest.fn()
|
||||
render(<Actions {...defaultProps} handleNextStep={handleNextStep} disabled={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSelectAll when checkbox is clicked', () => {
|
||||
// Arrange
|
||||
const onSelectAll = jest.fn()
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
onSelectAll={onSelectAll}
|
||||
totalOptions={5}
|
||||
selectedOptions={0}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Act - find the checkbox container and click it
|
||||
const selectAllLabel = screen.getByText('common.operation.selectAll')
|
||||
const checkboxContainer = selectAllLabel.closest('.flex.shrink-0.items-center')
|
||||
const checkbox = checkboxContainer?.querySelector('[class*="cursor-pointer"]')
|
||||
if (checkbox)
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
// Assert
|
||||
expect(onSelectAll).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Memoization Logic Testing
|
||||
// ==========================================
|
||||
describe('Memoization Logic', () => {
|
||||
// Tests for useMemo hooks (indeterminate and checked)
|
||||
describe('indeterminate calculation', () => {
|
||||
it('should return false when showSelect is false', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={false}
|
||||
totalOptions={5}
|
||||
selectedOptions={2}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox not rendered, so can't check indeterminate directly
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when selectedOptions is undefined', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={undefined}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should not be indeterminate
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when totalOptions is undefined', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={undefined}
|
||||
selectedOptions={2}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should exist
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return true when some options are selected (0 < selectedOptions < totalOptions)', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={3}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should render in indeterminate state
|
||||
// The checkbox component renders IndeterminateIcon when indeterminate and not checked
|
||||
const selectAllContainer = container.querySelector('.flex.shrink-0.items-center')
|
||||
expect(selectAllContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when no options are selected (selectedOptions === 0)', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={0}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should be unchecked and not indeterminate
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when all options are selected (selectedOptions === totalOptions)', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={5}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should be checked, not indeterminate
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('checked calculation', () => {
|
||||
it('should return false when showSelect is false', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={false}
|
||||
totalOptions={5}
|
||||
selectedOptions={5}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox not rendered
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when selectedOptions is undefined', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={undefined}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when totalOptions is undefined', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={undefined}
|
||||
selectedOptions={5}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return true when all options are selected (selectedOptions === totalOptions)', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={5}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should show checked state (RiCheckLine icon)
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when selectedOptions is 0', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={0}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should be unchecked
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return false when not all options are selected', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={4}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - checkbox should be indeterminate, not checked
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Component Memoization Testing
|
||||
// ==========================================
|
||||
describe('Component Memoization', () => {
|
||||
// Tests for React.memo behavior
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert - verify component has memo wrapper
|
||||
expect(Actions.$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
|
||||
it('should not re-render when props are the same', () => {
|
||||
// Arrange
|
||||
const handleNextStep = jest.fn()
|
||||
const props = {
|
||||
handleNextStep,
|
||||
disabled: false,
|
||||
showSelect: true,
|
||||
totalOptions: 5,
|
||||
selectedOptions: 3,
|
||||
onSelectAll: jest.fn(),
|
||||
tip: 'Test tip',
|
||||
}
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<Actions {...props} />)
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<Actions {...props} />)
|
||||
|
||||
// Assert - component renders correctly after rerender
|
||||
expect(screen.getByText('common.operation.selectAll')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test tip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when props change', () => {
|
||||
// Arrange
|
||||
const handleNextStep = jest.fn()
|
||||
const initialProps = {
|
||||
handleNextStep,
|
||||
disabled: false,
|
||||
showSelect: true,
|
||||
totalOptions: 5,
|
||||
selectedOptions: 0,
|
||||
onSelectAll: jest.fn(),
|
||||
tip: 'Initial tip',
|
||||
}
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<Actions {...initialProps} />)
|
||||
expect(screen.getByText('Initial tip')).toBeInTheDocument()
|
||||
|
||||
// Rerender with different props
|
||||
rerender(<Actions {...initialProps} tip="Updated tip" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Updated tip')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Initial tip')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases Testing
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
// Tests for boundary conditions and unusual inputs
|
||||
it('should handle totalOptions of 0', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={0}
|
||||
selectedOptions={0}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should render checkbox
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very large totalOptions', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={1000000}
|
||||
selectedOptions={500000}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long tip text', () => {
|
||||
// Arrange
|
||||
const longTip = 'A'.repeat(500)
|
||||
|
||||
// Act
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
tip={longTip}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - tip should render with truncate class
|
||||
const tipElement = screen.getByTitle(longTip)
|
||||
expect(tipElement).toHaveClass('truncate')
|
||||
})
|
||||
|
||||
it('should handle tip with special characters', () => {
|
||||
// Arrange
|
||||
const specialTip = '<script>alert("xss")</script> & "quotes" \'apostrophes\''
|
||||
|
||||
// Act
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
tip={specialTip}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - special characters should be rendered safely
|
||||
expect(screen.getByText(specialTip)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle tip with unicode characters', () => {
|
||||
// Arrange
|
||||
const unicodeTip = '选中 5 个文件,共 10MB 🚀'
|
||||
|
||||
// Act
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
tip={unicodeTip}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(unicodeTip)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle selectedOptions greater than totalOptions', () => {
|
||||
// This is an edge case that shouldn't happen but should be handled gracefully
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={10}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should still render
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle negative selectedOptions', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={-1}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should still render (though this is an invalid state)
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle onSelectAll being undefined when showSelect is true', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={3}
|
||||
onSelectAll={undefined}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should render checkbox
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
|
||||
// Click should not throw
|
||||
if (checkbox)
|
||||
expect(() => fireEvent.click(checkbox)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle empty datasetId from params', () => {
|
||||
// This test verifies the link is constructed even with empty datasetId
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert - link should still be present with the mocked datasetId
|
||||
const cancelLink = screen.getByRole('link')
|
||||
expect(cancelLink).toHaveAttribute('href', '/datasets/test-dataset-id/documents')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// All Prop Combinations Testing
|
||||
// ==========================================
|
||||
describe('Prop Combinations', () => {
|
||||
// Tests for various combinations of props
|
||||
it('should handle disabled=true with showSelect=false', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions {...defaultProps} disabled={true} showSelect={false} />)
|
||||
|
||||
// Assert
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })
|
||||
expect(nextButton).toBeDisabled()
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle disabled=true with showSelect=true', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
disabled={true}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={3}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })
|
||||
expect(nextButton).toBeDisabled()
|
||||
expect(screen.getByText('common.operation.selectAll')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render complete component with all props provided', () => {
|
||||
// Arrange
|
||||
const allProps = {
|
||||
disabled: false,
|
||||
handleNextStep: jest.fn(),
|
||||
showSelect: true,
|
||||
totalOptions: 10,
|
||||
selectedOptions: 5,
|
||||
onSelectAll: jest.fn(),
|
||||
tip: 'All props provided',
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Actions {...allProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.operation.selectAll')).toBeInTheDocument()
|
||||
expect(screen.getByText('All props provided')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should render minimal component with only required props', () => {
|
||||
// Arrange & Act
|
||||
render(<Actions handleNextStep={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('common.operation.selectAll')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('common.operation.cancel')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Selection State Variations Testing
|
||||
// ==========================================
|
||||
describe('Selection State Variations', () => {
|
||||
// Tests for different selection states
|
||||
const selectionStates = [
|
||||
{ totalOptions: 10, selectedOptions: 0, expectedState: 'unchecked' },
|
||||
{ totalOptions: 10, selectedOptions: 5, expectedState: 'indeterminate' },
|
||||
{ totalOptions: 10, selectedOptions: 10, expectedState: 'checked' },
|
||||
{ totalOptions: 1, selectedOptions: 0, expectedState: 'unchecked' },
|
||||
{ totalOptions: 1, selectedOptions: 1, expectedState: 'checked' },
|
||||
{ totalOptions: 100, selectedOptions: 1, expectedState: 'indeterminate' },
|
||||
{ totalOptions: 100, selectedOptions: 99, expectedState: 'indeterminate' },
|
||||
]
|
||||
|
||||
it.each(selectionStates)(
|
||||
'should render with $expectedState state when totalOptions=$totalOptions and selectedOptions=$selectedOptions',
|
||||
({ totalOptions, selectedOptions }) => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={totalOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - component should render without errors
|
||||
const checkbox = container.querySelector('[class*="cursor-pointer"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
expect(screen.getByText('common.operation.selectAll')).toBeInTheDocument()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Layout Structure Testing
|
||||
// ==========================================
|
||||
describe('Layout', () => {
|
||||
// Tests for correct layout structure
|
||||
it('should have correct container structure', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
const mainContainer = container.querySelector('.flex.items-center.gap-x-2.overflow-hidden')
|
||||
expect(mainContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct button container structure', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Actions {...defaultProps} />)
|
||||
|
||||
// Assert - buttons should be in a flex container
|
||||
const buttonContainer = container.querySelector('.flex.grow.items-center.justify-end.gap-x-2')
|
||||
expect(buttonContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should position select all section before buttons when showSelect is true', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Actions
|
||||
{...defaultProps}
|
||||
showSelect={true}
|
||||
totalOptions={5}
|
||||
selectedOptions={3}
|
||||
onSelectAll={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - select all section should exist
|
||||
const selectAllSection = container.querySelector('.flex.shrink-0.items-center')
|
||||
expect(selectAllSection).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,461 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import ChunkPreview from './chunk-preview'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Mock dataset-detail context - needs mock to control return values
|
||||
const mockDocForm = jest.fn()
|
||||
jest.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (_selector: (s: { dataset: { doc_form: ChunkingMode } }) => ChunkingMode) => {
|
||||
return mockDocForm()
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock document picker - needs mock for simplified interaction testing
|
||||
jest.mock('../../../common/document-picker/preview-document-picker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ files, onChange, value }: {
|
||||
files: Array<{ id: string; name: string; extension: string }>
|
||||
onChange: (selected: { id: string; name: string; extension: string }) => void
|
||||
value: { id: string; name: string; extension: string }
|
||||
}) => (
|
||||
<div data-testid="document-picker">
|
||||
<span data-testid="picker-value">{value?.name || 'No selection'}</span>
|
||||
<select
|
||||
data-testid="picker-select"
|
||||
value={value?.id || ''}
|
||||
onChange={(e) => {
|
||||
const selected = files.find(f => f.id === e.target.value)
|
||||
if (selected)
|
||||
onChange(selected)
|
||||
}}
|
||||
>
|
||||
{files.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Test data factories
|
||||
const createMockLocalFile = (overrides?: Partial<CustomFile>): CustomFile => ({
|
||||
id: 'file-1',
|
||||
name: 'test-file.pdf',
|
||||
size: 1024,
|
||||
type: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
lastModified: Date.now(),
|
||||
webkitRelativePath: '',
|
||||
arrayBuffer: jest.fn() as () => Promise<ArrayBuffer>,
|
||||
bytes: jest.fn() as () => Promise<Uint8Array>,
|
||||
slice: jest.fn() as (start?: number, end?: number, contentType?: string) => Blob,
|
||||
stream: jest.fn() as () => ReadableStream<Uint8Array>,
|
||||
text: jest.fn() as () => Promise<string>,
|
||||
...overrides,
|
||||
} as CustomFile)
|
||||
|
||||
const createMockNotionPage = (overrides?: Partial<NotionPage>): NotionPage => ({
|
||||
page_id: 'page-1',
|
||||
page_name: 'Test Page',
|
||||
workspace_id: 'workspace-1',
|
||||
type: 'page',
|
||||
page_icon: null,
|
||||
parent_id: 'parent-1',
|
||||
is_bound: true,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockCrawlResult = (overrides?: Partial<CrawlResultItem>): CrawlResultItem => ({
|
||||
title: 'Test Website',
|
||||
markdown: 'Test content',
|
||||
description: 'Test description',
|
||||
source_url: 'https://example.com',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockOnlineDriveFile = (overrides?: Partial<OnlineDriveFile>): OnlineDriveFile => ({
|
||||
id: 'drive-file-1',
|
||||
name: 'test-drive-file.docx',
|
||||
size: 2048,
|
||||
type: OnlineDriveFileType.file,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockEstimateData = (overrides?: Partial<FileIndexingEstimateResponse>): FileIndexingEstimateResponse => ({
|
||||
total_nodes: 5,
|
||||
tokens: 1000,
|
||||
total_price: 0.01,
|
||||
currency: 'USD',
|
||||
total_segments: 10,
|
||||
preview: [
|
||||
{ content: 'Chunk content 1', child_chunks: ['child 1', 'child 2'] },
|
||||
{ content: 'Chunk content 2', child_chunks: ['child 3'] },
|
||||
],
|
||||
qa_preview: [
|
||||
{ question: 'Q1', answer: 'A1' },
|
||||
{ question: 'Q2', answer: 'A2' },
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
dataSourceType: DatasourceType.localFile,
|
||||
localFiles: [createMockLocalFile()],
|
||||
onlineDocuments: [createMockNotionPage()],
|
||||
websitePages: [createMockCrawlResult()],
|
||||
onlineDriveFiles: [createMockOnlineDriveFile()],
|
||||
isIdle: false,
|
||||
isPending: false,
|
||||
estimateData: undefined,
|
||||
onPreview: jest.fn(),
|
||||
handlePreviewFileChange: jest.fn(),
|
||||
handlePreviewOnlineDocumentChange: jest.fn(),
|
||||
handlePreviewWebsitePageChange: jest.fn(),
|
||||
handlePreviewOnlineDriveFileChange: jest.fn(),
|
||||
}
|
||||
|
||||
describe('ChunkPreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with preview container', () => {
|
||||
render(<ChunkPreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for local files', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.localFile} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for online documents', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.onlineDocument} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for website pages', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.websiteCrawl} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for online drive files', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.onlineDrive} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render badge with chunk count for non-QA mode', () => {
|
||||
const estimateData = createMockEstimateData({ total_segments: 15 })
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
// Badge shows chunk count via i18n key with count option
|
||||
expect(screen.getByText(/previewChunkCount.*15/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render badge for QA mode', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.qa)
|
||||
const estimateData = createMockEstimateData()
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
// No badge with total_segments
|
||||
expect(screen.queryByText(/10/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Idle State', () => {
|
||||
it('should render idle state with preview tip and button', () => {
|
||||
render(<ChunkPreview {...defaultProps} isIdle={true} />)
|
||||
|
||||
// i18n mock returns keys
|
||||
expect(screen.getByText('datasetCreation.stepTwo.previewChunkTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.previewChunks')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
const onPreview = jest.fn()
|
||||
|
||||
render(<ChunkPreview {...defaultProps} isIdle={true} onPreview={onPreview} />)
|
||||
|
||||
const button = screen.getByRole('button', { name: /previewChunks/i })
|
||||
fireEvent.click(button)
|
||||
expect(onPreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render skeleton loading when isPending is true', () => {
|
||||
render(<ChunkPreview {...defaultProps} isPending={true} />)
|
||||
|
||||
// Skeleton loading renders multiple skeleton containers
|
||||
expect(document.querySelector('.space-y-6')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render preview content when loading', () => {
|
||||
const estimateData = createMockEstimateData()
|
||||
|
||||
render(<ChunkPreview {...defaultProps} isPending={true} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Chunk content 1')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('QA Mode Preview', () => {
|
||||
it('should render QA preview chunks when doc_form is qa', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.qa)
|
||||
const estimateData = createMockEstimateData({
|
||||
qa_preview: [
|
||||
{ question: 'Question 1?', answer: 'Answer 1' },
|
||||
{ question: 'Question 2?', answer: 'Answer 2' },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.getByText('Question 1?')).toBeInTheDocument()
|
||||
expect(screen.getByText('Answer 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Question 2?')).toBeInTheDocument()
|
||||
expect(screen.getByText('Answer 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Text Mode Preview', () => {
|
||||
it('should render text preview chunks when doc_form is text', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
const estimateData = createMockEstimateData({
|
||||
preview: [
|
||||
{ content: 'Text chunk 1', child_chunks: [] },
|
||||
{ content: 'Text chunk 2', child_chunks: [] },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.getByText('Text chunk 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Text chunk 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parent-Child Mode Preview', () => {
|
||||
it('should render parent-child preview chunks', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.parentChild)
|
||||
const estimateData = createMockEstimateData({
|
||||
preview: [
|
||||
{ content: 'Parent chunk 1', child_chunks: ['Child 1', 'Child 2'] },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.getByText('Child 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Child 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Document Selection', () => {
|
||||
it('should handle local file selection change', () => {
|
||||
const handlePreviewFileChange = jest.fn()
|
||||
const localFiles = [
|
||||
createMockLocalFile({ id: 'file-1', name: 'file1.pdf' }),
|
||||
createMockLocalFile({ id: 'file-2', name: 'file2.pdf' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.localFile}
|
||||
localFiles={localFiles}
|
||||
handlePreviewFileChange={handlePreviewFileChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'file-2' } })
|
||||
|
||||
expect(handlePreviewFileChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle online document selection change', () => {
|
||||
const handlePreviewOnlineDocumentChange = jest.fn()
|
||||
const onlineDocuments = [
|
||||
createMockNotionPage({ page_id: 'page-1', page_name: 'Page 1' }),
|
||||
createMockNotionPage({ page_id: 'page-2', page_name: 'Page 2' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDocument}
|
||||
onlineDocuments={onlineDocuments}
|
||||
handlePreviewOnlineDocumentChange={handlePreviewOnlineDocumentChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'page-2' } })
|
||||
|
||||
expect(handlePreviewOnlineDocumentChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle website page selection change', () => {
|
||||
const handlePreviewWebsitePageChange = jest.fn()
|
||||
const websitePages = [
|
||||
createMockCrawlResult({ source_url: 'https://example1.com', title: 'Site 1' }),
|
||||
createMockCrawlResult({ source_url: 'https://example2.com', title: 'Site 2' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.websiteCrawl}
|
||||
websitePages={websitePages}
|
||||
handlePreviewWebsitePageChange={handlePreviewWebsitePageChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'https://example2.com' } })
|
||||
|
||||
expect(handlePreviewWebsitePageChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle online drive file selection change', () => {
|
||||
const handlePreviewOnlineDriveFileChange = jest.fn()
|
||||
const onlineDriveFiles = [
|
||||
createMockOnlineDriveFile({ id: 'drive-1', name: 'file1.docx' }),
|
||||
createMockOnlineDriveFile({ id: 'drive-2', name: 'file2.docx' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDrive}
|
||||
onlineDriveFiles={onlineDriveFiles}
|
||||
handlePreviewOnlineDriveFileChange={handlePreviewOnlineDriveFileChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'drive-2' } })
|
||||
|
||||
expect(handlePreviewOnlineDriveFileChange).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty estimate data', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={undefined} />)
|
||||
|
||||
expect(screen.queryByText('Chunk content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty preview array', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
const estimateData = createMockEstimateData({ preview: [] })
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Chunk content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty qa_preview array', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.qa)
|
||||
const estimateData = createMockEstimateData({ qa_preview: [] })
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Q1')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty child_chunks in parent-child mode', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.parentChild)
|
||||
const estimateData = createMockEstimateData({
|
||||
preview: [{ content: 'Parent', child_chunks: [] }],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Child')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle badge showing 0 chunks', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
const estimateData = createMockEstimateData({ total_segments: 0 })
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
// Badge with 0
|
||||
expect(screen.getByText(/0/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined online document properties', () => {
|
||||
const onlineDocuments = [createMockNotionPage({ page_id: '', page_name: '' })]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDocument}
|
||||
onlineDocuments={onlineDocuments}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined website page properties', () => {
|
||||
const websitePages = [createMockCrawlResult({ source_url: '', title: '' })]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.websiteCrawl}
|
||||
websitePages={websitePages}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined online drive file properties', () => {
|
||||
const onlineDriveFiles = [createMockOnlineDriveFile({ id: '', name: '' })]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDrive}
|
||||
onlineDriveFiles={onlineDriveFiles}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Memoization', () => {
|
||||
it('should be exported as a memoized component', () => {
|
||||
// ChunkPreview is wrapped with React.memo
|
||||
// We verify this by checking the component type
|
||||
expect(typeof ChunkPreview).toBe('object')
|
||||
expect(ChunkPreview.$$typeof?.toString()).toBe('Symbol(react.memo)')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,320 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import FilePreview from './file-preview'
|
||||
import type { CustomFile as File } from '@/models/datasets'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Mock useFilePreview hook - needs to be mocked to control return values
|
||||
const mockUseFilePreview = jest.fn()
|
||||
jest.mock('@/service/use-common', () => ({
|
||||
useFilePreview: (fileID: string) => mockUseFilePreview(fileID),
|
||||
}))
|
||||
|
||||
// Test data factory
|
||||
const createMockFile = (overrides?: Partial<File>): File => ({
|
||||
id: 'file-123',
|
||||
name: 'test-document.pdf',
|
||||
size: 2048,
|
||||
type: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
lastModified: Date.now(),
|
||||
webkitRelativePath: '',
|
||||
arrayBuffer: jest.fn() as () => Promise<ArrayBuffer>,
|
||||
bytes: jest.fn() as () => Promise<Uint8Array>,
|
||||
slice: jest.fn() as (start?: number, end?: number, contentType?: string) => Blob,
|
||||
stream: jest.fn() as () => ReadableStream<Uint8Array>,
|
||||
text: jest.fn() as () => Promise<string>,
|
||||
...overrides,
|
||||
} as File)
|
||||
|
||||
const createMockFilePreviewData = (content: string = 'This is the file content') => ({
|
||||
content,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
file: createMockFile(),
|
||||
hidePreview: jest.fn(),
|
||||
}
|
||||
|
||||
describe('FilePreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with file information', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('test-document.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display file extension in uppercase via CSS class', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// The extension is displayed in the info section (as uppercase via CSS class)
|
||||
const extensionElement = screen.getByText('pdf')
|
||||
expect(extensionElement).toBeInTheDocument()
|
||||
expect(extensionElement).toHaveClass('uppercase')
|
||||
})
|
||||
|
||||
it('should display formatted file size', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatFileSize: 2048 bytes => "2.00 KB"
|
||||
expect(screen.getByText('2.00 KB')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call useFilePreview with correct fileID', () => {
|
||||
const file = createMockFile({ id: 'specific-file-id' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(mockUseFilePreview).toHaveBeenCalledWith('specific-file-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('File Name Processing', () => {
|
||||
it('should extract file name without extension', () => {
|
||||
const file = createMockFile({ name: 'my-document.pdf', extension: 'pdf' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// The displayed text is `${fileName}.${extension}`, where fileName is name without ext
|
||||
// my-document.pdf -> fileName = 'my-document', displayed as 'my-document.pdf'
|
||||
expect(screen.getByText('my-document.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle file name with multiple dots', () => {
|
||||
const file = createMockFile({ name: 'my.file.name.pdf', extension: 'pdf' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// fileName = arr.slice(0, -1).join() = 'my,file,name', then displayed as 'my,file,name.pdf'
|
||||
expect(screen.getByText('my,file,name.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty file name', () => {
|
||||
const file = createMockFile({ name: '', extension: '' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// fileName = '', displayed as '.'
|
||||
expect(screen.getByText('.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle file without extension in name', () => {
|
||||
const file = createMockFile({ name: 'noextension', extension: '' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// fileName = '' (slice returns empty for single element array), displayed as '.'
|
||||
expect(screen.getByText('.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render loading component when fetching', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: true,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Loading component renders skeleton
|
||||
expect(document.querySelector('.overflow-hidden')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render content when loading', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData('Some content'),
|
||||
isFetching: true,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByText('Some content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Display', () => {
|
||||
it('should render file content when loaded', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData('This is the file content'),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('This is the file content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display character count when data is available', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData('Hello'), // 5 characters
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format large character counts', () => {
|
||||
const longContent = 'a'.repeat(2500)
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData(longContent),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
||||
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not display character count when data is not available', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// No character text shown
|
||||
expect(screen.queryByText(/datasetPipeline\.addDocuments\.characters/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = jest.fn()
|
||||
|
||||
render(<FilePreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('File Size Formatting', () => {
|
||||
it('should format small file sizes in bytes', () => {
|
||||
const file = createMockFile({ size: 500 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize: 500 => "500.00 bytes"
|
||||
expect(screen.getByText('500.00 bytes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format kilobyte file sizes', () => {
|
||||
const file = createMockFile({ size: 5120 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize: 5120 => "5.00 KB"
|
||||
expect(screen.getByText('5.00 KB')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format megabyte file sizes', () => {
|
||||
const file = createMockFile({ size: 2097152 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize: 2097152 => "2.00 MB"
|
||||
expect(screen.getByText('2.00 MB')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined file id', () => {
|
||||
const file = createMockFile({ id: undefined })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(mockUseFilePreview).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should handle empty extension', () => {
|
||||
const file = createMockFile({ extension: undefined })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle zero file size', () => {
|
||||
const file = createMockFile({ size: 0 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize returns 0 for falsy values
|
||||
// The component still renders
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long file content', () => {
|
||||
const veryLongContent = 'a'.repeat(1000000)
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData(veryLongContent),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated: 1000000 => "1M"
|
||||
expect(screen.getByText(/1M/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData(''),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated: 0 => "0"
|
||||
// Find the element that contains character count info
|
||||
expect(screen.getByText(/0 datasetPipeline/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMemo for fileName', () => {
|
||||
it('should extract file name when file exists', () => {
|
||||
// When file exists, it should extract the name without extension
|
||||
const file = createMockFile({ name: 'document.txt', extension: 'txt' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByText('document.txt')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should memoize fileName based on file prop', () => {
|
||||
const file = createMockFile({ name: 'test.pdf', extension: 'pdf' })
|
||||
|
||||
const { rerender } = render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Same file should produce same result
|
||||
rerender(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByText('test.pdf')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,359 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import OnlineDocumentPreview from './online-document-preview'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Spy on Toast.notify
|
||||
const toastNotifySpy = jest.spyOn(Toast, 'notify')
|
||||
|
||||
// Mock dataset-detail context - needs mock to control return values
|
||||
const mockPipelineId = jest.fn()
|
||||
jest.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (_selector: (s: { dataset: { pipeline_id: string } }) => string) => {
|
||||
return mockPipelineId()
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock usePreviewOnlineDocument hook - needs mock to control mutation behavior
|
||||
const mockMutateAsync = jest.fn()
|
||||
const mockUsePreviewOnlineDocument = jest.fn()
|
||||
jest.mock('@/service/use-pipeline', () => ({
|
||||
usePreviewOnlineDocument: () => mockUsePreviewOnlineDocument(),
|
||||
}))
|
||||
|
||||
// Mock data source store - needs mock to control store state
|
||||
const mockCurrentCredentialId = 'credential-123'
|
||||
const mockGetState = jest.fn(() => ({
|
||||
currentCredentialId: mockCurrentCredentialId,
|
||||
}))
|
||||
jest.mock('../data-source/store', () => ({
|
||||
useDataSourceStore: () => ({
|
||||
getState: mockGetState,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Test data factory
|
||||
const createMockNotionPage = (overrides?: Partial<NotionPage>): NotionPage => ({
|
||||
page_id: 'page-123',
|
||||
page_name: 'Test Notion Page',
|
||||
workspace_id: 'workspace-456',
|
||||
type: 'page',
|
||||
page_icon: null,
|
||||
parent_id: 'parent-789',
|
||||
is_bound: true,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
currentPage: createMockNotionPage(),
|
||||
datasourceNodeId: 'datasource-node-123',
|
||||
hidePreview: jest.fn(),
|
||||
}
|
||||
|
||||
describe('OnlineDocumentPreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPipelineId.mockReturnValue('pipeline-123')
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
})
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Test content' })
|
||||
return Promise.resolve({ content: 'Test content' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with page information', () => {
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Notion Page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display page type', () => {
|
||||
const currentPage = createMockNotionPage({ type: 'database' })
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
||||
|
||||
expect(screen.getByText('database')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data Fetching', () => {
|
||||
it('should call mutateAsync with correct parameters on mount', async () => {
|
||||
const currentPage = createMockNotionPage({
|
||||
workspace_id: 'ws-123',
|
||||
page_id: 'pg-456',
|
||||
type: 'page',
|
||||
})
|
||||
|
||||
render(
|
||||
<OnlineDocumentPreview
|
||||
{...defaultProps}
|
||||
currentPage={currentPage}
|
||||
datasourceNodeId="node-789"
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceID: 'ws-123',
|
||||
pageID: 'pg-456',
|
||||
pageType: 'page',
|
||||
pipelineId: 'pipeline-123',
|
||||
datasourceNodeId: 'node-789',
|
||||
credentialId: mockCurrentCredentialId,
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should fetch data again when page_id changes', async () => {
|
||||
const currentPage1 = createMockNotionPage({ page_id: 'page-1' })
|
||||
const currentPage2 = createMockNotionPage({ page_id: 'page-2' })
|
||||
|
||||
const { rerender } = render(
|
||||
<OnlineDocumentPreview {...defaultProps} currentPage={currentPage1} />,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
rerender(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage2} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty pipelineId', async () => {
|
||||
mockPipelineId.mockReturnValue(undefined)
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pipelineId: '',
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render loading component when isPending is true', () => {
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// Loading component renders skeleton
|
||||
expect(document.querySelector('.overflow-hidden')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render markdown content when loading', () => {
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// Content area should not be present
|
||||
expect(screen.queryByText('Test content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Display', () => {
|
||||
it('should render markdown content when loaded', async () => {
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Markdown content here' })
|
||||
return Promise.resolve({ content: 'Markdown content here' })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
// Markdown component renders the content
|
||||
const contentArea = document.querySelector('.overflow-hidden.px-6.py-5')
|
||||
expect(contentArea).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should display character count', async () => {
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Hello' }) // 5 characters
|
||||
return Promise.resolve({ content: 'Hello' })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should format large character counts', async () => {
|
||||
const longContent = 'a'.repeat(2500)
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: longContent })
|
||||
return Promise.resolve({ content: longContent })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
||||
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show character count based on fetched content', async () => {
|
||||
// When content is set via onSuccess, character count is displayed
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Test content' }) // 12 characters
|
||||
return Promise.resolve({ content: 'Test content' })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/12/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should show toast notification on error', async () => {
|
||||
const errorMessage = 'Failed to fetch document'
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onError(new Error(errorMessage))
|
||||
// Return a resolved promise to avoid unhandled rejection
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
const networkError = new Error('Network Error')
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onError(networkError)
|
||||
// Return a resolved promise to avoid unhandled rejection
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Network Error',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = jest.fn()
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
// Find the close button in the header area (not toast buttons)
|
||||
const headerArea = document.querySelector('.flex.gap-x-2.border-b')
|
||||
const closeButton = headerArea?.querySelector('button')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined page_name', () => {
|
||||
const currentPage = createMockNotionPage({ page_name: '' })
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
||||
|
||||
// Find the close button in the header area
|
||||
const headerArea = document.querySelector('.flex.gap-x-2.border-b')
|
||||
const closeButton = headerArea?.querySelector('button')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle different page types', () => {
|
||||
const currentPage = createMockNotionPage({ type: 'database' })
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
||||
|
||||
expect(screen.getByText('database')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use credentialId from store', async () => {
|
||||
mockGetState.mockReturnValue({
|
||||
currentCredentialId: 'custom-credential',
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
credentialId: 'custom-credential',
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render markdown content when content is empty and not pending', async () => {
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: '' })
|
||||
return Promise.resolve({ content: '' })
|
||||
})
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// Content is empty, markdown area should still render but be empty
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Test content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,256 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import WebsitePreview from './web-preview'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Test data factory
|
||||
const createMockCrawlResult = (overrides?: Partial<CrawlResultItem>): CrawlResultItem => ({
|
||||
title: 'Test Website Title',
|
||||
markdown: 'This is the **markdown** content of the website.',
|
||||
description: 'Test description',
|
||||
source_url: 'https://example.com/page',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
currentWebsite: createMockCrawlResult(),
|
||||
hidePreview: jest.fn(),
|
||||
}
|
||||
|
||||
describe('WebsitePreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with website information', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Website Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display the source URL', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the markdown content', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('This is the **markdown** content of the website.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Character Count', () => {
|
||||
it('should display character count for small content', () => {
|
||||
const currentWebsite = createMockCrawlResult({ markdown: 'Hello' }) // 5 characters
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format character count in thousands', () => {
|
||||
const longContent = 'a'.repeat(2500)
|
||||
const currentWebsite = createMockCrawlResult({ markdown: longContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
||||
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format character count in millions', () => {
|
||||
const veryLongContent = 'a'.repeat(1500000)
|
||||
const currentWebsite = createMockCrawlResult({ markdown: veryLongContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(/1\.5M/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show 0 characters for empty markdown', () => {
|
||||
const currentWebsite = createMockCrawlResult({ markdown: '' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(/0/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = jest.fn()
|
||||
|
||||
render(<WebsitePreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('URL Display', () => {
|
||||
it('should display long URLs', () => {
|
||||
const longUrl = 'https://example.com/very/long/path/to/page/with/many/segments'
|
||||
const currentWebsite = createMockCrawlResult({ source_url: longUrl })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
const urlElement = screen.getByTitle(longUrl)
|
||||
expect(urlElement).toBeInTheDocument()
|
||||
expect(urlElement).toHaveTextContent(longUrl)
|
||||
})
|
||||
|
||||
it('should display URL with title attribute', () => {
|
||||
const currentWebsite = createMockCrawlResult({ source_url: 'https://test.com' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByTitle('https://test.com')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Display', () => {
|
||||
it('should display the markdown content in content area', () => {
|
||||
const currentWebsite = createMockCrawlResult({
|
||||
markdown: 'Content with **bold** and *italic* text.',
|
||||
})
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText('Content with **bold** and *italic* text.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiline content', () => {
|
||||
const multilineContent = 'Line 1\nLine 2\nLine 3'
|
||||
const currentWebsite = createMockCrawlResult({ markdown: multilineContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
// Multiline content is rendered as-is
|
||||
expect(screen.getByText((content) => {
|
||||
return content.includes('Line 1') && content.includes('Line 2') && content.includes('Line 3')
|
||||
})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in content', () => {
|
||||
const specialContent = '<script>alert("xss")</script> & < > " \''
|
||||
const currentWebsite = createMockCrawlResult({ markdown: specialContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(specialContent)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty title', () => {
|
||||
const currentWebsite = createMockCrawlResult({ title: '' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty source URL', () => {
|
||||
const currentWebsite = createMockCrawlResult({ source_url: '' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long title', () => {
|
||||
const longTitle = 'A'.repeat(500)
|
||||
const currentWebsite = createMockCrawlResult({ title: longTitle })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(longTitle)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle unicode characters in content', () => {
|
||||
const unicodeContent = '你好世界 🌍 مرحبا こんにちは'
|
||||
const currentWebsite = createMockCrawlResult({ markdown: unicodeContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle URL with query parameters', () => {
|
||||
const urlWithParams = 'https://example.com/page?query=test¶m=value'
|
||||
const currentWebsite = createMockCrawlResult({ source_url: urlWithParams })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByTitle(urlWithParams)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle URL with hash fragment', () => {
|
||||
const urlWithHash = 'https://example.com/page#section-1'
|
||||
const currentWebsite = createMockCrawlResult({ source_url: urlWithHash })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByTitle(urlWithHash)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply container styles', () => {
|
||||
const { container } = render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
const mainContainer = container.firstChild as HTMLElement
|
||||
expect(mainContainer).toHaveClass('flex', 'h-full', 'w-full', 'flex-col')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multiple Renders', () => {
|
||||
it('should update when currentWebsite changes', () => {
|
||||
const website1 = createMockCrawlResult({ title: 'Website 1', markdown: 'Content 1' })
|
||||
const website2 = createMockCrawlResult({ title: 'Website 2', markdown: 'Content 2' })
|
||||
|
||||
const { rerender } = render(<WebsitePreview {...defaultProps} currentWebsite={website1} />)
|
||||
|
||||
expect(screen.getByText('Website 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Content 1')).toBeInTheDocument()
|
||||
|
||||
rerender(<WebsitePreview {...defaultProps} currentWebsite={website2} />)
|
||||
|
||||
expect(screen.getByText('Website 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Content 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call new hidePreview when prop changes', () => {
|
||||
const hidePreview1 = jest.fn()
|
||||
const hidePreview2 = jest.fn()
|
||||
|
||||
const { rerender } = render(<WebsitePreview {...defaultProps} hidePreview={hidePreview1} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
expect(hidePreview1).toHaveBeenCalledTimes(1)
|
||||
|
||||
rerender(<WebsitePreview {...defaultProps} hidePreview={hidePreview2} />)
|
||||
|
||||
fireEvent.click(closeButton)
|
||||
expect(hidePreview2).toHaveBeenCalledTimes(1)
|
||||
expect(hidePreview1).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,861 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Actions from './actions'
|
||||
import Header from './header'
|
||||
import Form from './form'
|
||||
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { z } from 'zod'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
// ==========================================
|
||||
// Spy on Toast.notify for validation tests
|
||||
// ==========================================
|
||||
const toastNotifySpy = jest.spyOn(Toast, 'notify')
|
||||
|
||||
// ==========================================
|
||||
// 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: 'Enter value',
|
||||
tooltip: '',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a valid Zod schema for testing
|
||||
*/
|
||||
const createMockSchema = () => {
|
||||
return z.object({
|
||||
field1: z.string().optional(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a schema that always fails validation
|
||||
*/
|
||||
const createFailingSchema = () => {
|
||||
return {
|
||||
safeParse: () => ({
|
||||
success: false,
|
||||
error: {
|
||||
issues: [{ path: ['field1'], message: 'is required' }],
|
||||
},
|
||||
}),
|
||||
} as unknown as z.ZodSchema
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Actions Component Tests
|
||||
// ==========================================
|
||||
describe('Actions', () => {
|
||||
const defaultActionsProps = {
|
||||
onBack: jest.fn(),
|
||||
onProcess: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.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 = jest.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 = jest.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 = jest.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: jest.fn(),
|
||||
resetDisabled: false,
|
||||
previewDisabled: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.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()
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
catch {
|
||||
didThrow = true
|
||||
}
|
||||
expect(didThrow).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions Testing
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onReset when reset button is clicked', () => {
|
||||
// Arrange
|
||||
const onReset = jest.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 = jest.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 = jest.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 = jest.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()
|
||||
expect(previewButton).toBeDisabled()
|
||||
})
|
||||
|
||||
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()
|
||||
expect(previewButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Form Component Tests
|
||||
// ==========================================
|
||||
describe('Form', () => {
|
||||
const defaultFormProps = {
|
||||
initialData: { field1: '' },
|
||||
configurations: [] as BaseConfiguration[],
|
||||
schema: createMockSchema(),
|
||||
onSubmit: jest.fn(),
|
||||
onPreview: jest.fn(),
|
||||
ref: { current: null } as React.RefObject<unknown>,
|
||||
isRunning: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
const fieldsContainer = container.querySelector('.flex.flex-col.gap-3')
|
||||
expect(fieldsContainer?.children.length).toBe(0)
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
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 = jest.fn()
|
||||
const mockRef = { current: null } as React.MutableRefObject<{ submit: () => void } | null>
|
||||
render(<Form {...defaultFormProps} ref={mockRef} onSubmit={onSubmit} />)
|
||||
|
||||
// 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} />)
|
||||
|
||||
// Act - call submit via ref
|
||||
mockRef.current?.submit()
|
||||
|
||||
// Assert - validation error should be shown
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: '"field1" is required',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions Testing
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
// Arrange
|
||||
const onPreview = jest.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 = jest.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' }),
|
||||
]
|
||||
|
||||
render(<Form {...defaultFormProps} configurations={configurations} />)
|
||||
|
||||
// Act - change input to make form dirty
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
it('should reset form to initial values when reset button is clicked', async () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Field 1' }),
|
||||
]
|
||||
const initialData = { field1: 'initial value' }
|
||||
|
||||
render(<Form {...defaultFormProps} configurations={configurations} initialData={initialData} />)
|
||||
|
||||
// Act - change input to make form dirty
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'new value' } })
|
||||
|
||||
// Wait for reset button to be enabled
|
||||
await waitFor(() => {
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
expect(resetButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Click reset button
|
||||
const resetButton = screen.getByRole('button', { name: /common.operation.reset/i })
|
||||
fireEvent.click(resetButton)
|
||||
|
||||
// Assert - form should be reset, button should be disabled again
|
||||
await waitFor(() => {
|
||||
expect(resetButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call form.reset when handleReset is triggered', async () => {
|
||||
// Arrange
|
||||
const configurations = [
|
||||
createMockConfiguration({ variable: 'field1', label: 'Field 1' }),
|
||||
]
|
||||
const initialData = { field1: 'original' }
|
||||
|
||||
render(<Form {...defaultFormProps} configurations={configurations} initialData={initialData} />)
|
||||
|
||||
// Make form dirty
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'modified' } })
|
||||
|
||||
// Wait for dirty state
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /common.operation.reset/i })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Act - click reset
|
||||
fireEvent.click(screen.getByRole('button', { name: /common.operation.reset/i }))
|
||||
|
||||
// Assert - input should be reset to initial value
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveValue('original')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// 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',
|
||||
message: '"field1" is required',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call onSubmit when validation fails', async () => {
|
||||
// Arrange
|
||||
const onSubmit = jest.fn()
|
||||
const failingSchema = createFailingSchema()
|
||||
const { container } = render(<Form {...defaultFormProps} schema={failingSchema} onSubmit={onSubmit} />)
|
||||
|
||||
// Act
|
||||
const form = container.querySelector('form')!
|
||||
fireEvent.submit(form)
|
||||
|
||||
// Assert - wait a bit and verify onSubmit was not called
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalled()
|
||||
})
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSubmit when validation passes', async () => {
|
||||
// Arrange
|
||||
const onSubmit = jest.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(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Form with Header Integration', () => {
|
||||
const defaultFormProps = {
|
||||
initialData: { field1: '' },
|
||||
configurations: [] as BaseConfiguration[],
|
||||
schema: createMockSchema(),
|
||||
onSubmit: jest.fn(),
|
||||
onPreview: jest.fn(),
|
||||
ref: { current: null } as React.RefObject<unknown>,
|
||||
isRunning: false,
|
||||
}
|
||||
|
||||
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,601 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,475 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import RuleDetail from './rule-detail'
|
||||
import { ProcessMode, type ProcessRuleResponse } from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
|
||||
// ==========================================
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock next/image (using img element for simplicity in tests)
|
||||
jest.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: function MockImage({ src, alt, className }: { src: string; alt: string; className?: string }) {
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={src} alt={alt} className={className} data-testid="next-image" />
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock FieldInfo component
|
||||
jest.mock('@/app/components/datasets/documents/detail/metadata', () => ({
|
||||
FieldInfo: ({ label, displayedValue, valueIcon }: { label: string; displayedValue: string; valueIcon?: React.ReactNode }) => (
|
||||
<div data-testid="field-info" data-label={label}>
|
||||
<span data-testid="field-label">{label}</span>
|
||||
<span data-testid="field-value">{displayedValue}</span>
|
||||
{valueIcon && <span data-testid="field-icon">{valueIcon}</span>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock icons - provides simple string paths for testing instead of Next.js static import objects
|
||||
jest.mock('@/app/components/datasets/create/icons', () => ({
|
||||
indexMethodIcon: {
|
||||
economical: '/icons/economical.svg',
|
||||
high_quality: '/icons/high_quality.svg',
|
||||
},
|
||||
retrievalIcon: {
|
||||
fullText: '/icons/fullText.svg',
|
||||
hybrid: '/icons/hybrid.svg',
|
||||
vector: '/icons/vector.svg',
|
||||
},
|
||||
}))
|
||||
|
||||
// ==========================================
|
||||
// Test Data Factory Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Creates a mock ProcessRuleResponse for testing
|
||||
*/
|
||||
const createMockProcessRule = (overrides: Partial<ProcessRuleResponse> = {}): ProcessRuleResponse => ({
|
||||
mode: ProcessMode.general,
|
||||
rules: {
|
||||
pre_processing_rules: [],
|
||||
segmentation: {
|
||||
separator: '\n',
|
||||
max_tokens: 500,
|
||||
chunk_overlap: 50,
|
||||
},
|
||||
parent_mode: 'paragraph',
|
||||
subchunk_segmentation: {
|
||||
separator: '\n',
|
||||
max_tokens: 200,
|
||||
chunk_overlap: 20,
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
indexing_max_segmentation_tokens_length: 1000,
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Test Suite
|
||||
// ==========================================
|
||||
|
||||
describe('RuleDetail', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail />)
|
||||
|
||||
// Assert
|
||||
const fieldInfos = screen.getAllByTestId('field-info')
|
||||
expect(fieldInfos).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should render three FieldInfo components', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={sourceData}
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldInfos = screen.getAllByTestId('field-info')
|
||||
expect(fieldInfos).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should render mode field with correct label', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail />)
|
||||
|
||||
// Assert - first field-info is for mode
|
||||
const fieldInfos = screen.getAllByTestId('field-info')
|
||||
expect(fieldInfos[0]).toHaveAttribute('data-label', 'datasetDocuments.embedding.mode')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Mode Value Tests
|
||||
// ==========================================
|
||||
describe('Mode Value', () => {
|
||||
it('should show "-" when sourceData is undefined', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[0]).toHaveTextContent('-')
|
||||
})
|
||||
|
||||
it('should show "-" when sourceData.mode is undefined', () => {
|
||||
// Arrange
|
||||
const sourceData = { ...createMockProcessRule(), mode: undefined as unknown as ProcessMode }
|
||||
|
||||
// Act
|
||||
render(<RuleDetail sourceData={sourceData} />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[0]).toHaveTextContent('-')
|
||||
})
|
||||
|
||||
it('should show custom mode text when mode is general', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule({ mode: ProcessMode.general })
|
||||
|
||||
// Act
|
||||
render(<RuleDetail sourceData={sourceData} />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[0]).toHaveTextContent('datasetDocuments.embedding.custom')
|
||||
})
|
||||
|
||||
it('should show hierarchical mode with paragraph parent mode', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule({
|
||||
mode: ProcessMode.parentChild,
|
||||
rules: {
|
||||
pre_processing_rules: [],
|
||||
segmentation: { separator: '\n', max_tokens: 500, chunk_overlap: 50 },
|
||||
parent_mode: 'paragraph',
|
||||
subchunk_segmentation: { separator: '\n', max_tokens: 200, chunk_overlap: 20 },
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RuleDetail sourceData={sourceData} />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[0]).toHaveTextContent('datasetDocuments.embedding.hierarchical · dataset.parentMode.paragraph')
|
||||
})
|
||||
|
||||
it('should show hierarchical mode with full-doc parent mode', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule({
|
||||
mode: ProcessMode.parentChild,
|
||||
rules: {
|
||||
pre_processing_rules: [],
|
||||
segmentation: { separator: '\n', max_tokens: 500, chunk_overlap: 50 },
|
||||
parent_mode: 'full-doc',
|
||||
subchunk_segmentation: { separator: '\n', max_tokens: 200, chunk_overlap: 20 },
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RuleDetail sourceData={sourceData} />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[0]).toHaveTextContent('datasetDocuments.embedding.hierarchical · dataset.parentMode.fullDoc')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Indexing Type Tests
|
||||
// ==========================================
|
||||
describe('Indexing Type', () => {
|
||||
it('should show qualified indexing type', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
|
||||
// Assert
|
||||
const fieldInfos = screen.getAllByTestId('field-info')
|
||||
expect(fieldInfos[1]).toHaveAttribute('data-label', 'datasetCreation.stepTwo.indexMode')
|
||||
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[1]).toHaveTextContent('datasetCreation.stepTwo.qualified')
|
||||
})
|
||||
|
||||
it('should show economical indexing type', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail indexingType={IndexingType.ECONOMICAL} />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[1]).toHaveTextContent('datasetCreation.stepTwo.economical')
|
||||
})
|
||||
|
||||
it('should show high_quality icon for qualified indexing', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
|
||||
// Assert
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
expect(images[0]).toHaveAttribute('src', '/icons/high_quality.svg')
|
||||
})
|
||||
|
||||
it('should show economical icon for economical indexing', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail indexingType={IndexingType.ECONOMICAL} />)
|
||||
|
||||
// Assert
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
expect(images[0]).toHaveAttribute('src', '/icons/economical.svg')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Retrieval Method Tests
|
||||
// ==========================================
|
||||
describe('Retrieval Method', () => {
|
||||
it('should show retrieval setting label', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail retrievalMethod={RETRIEVE_METHOD.semantic} />)
|
||||
|
||||
// Assert
|
||||
const fieldInfos = screen.getAllByTestId('field-info')
|
||||
expect(fieldInfos[2]).toHaveAttribute('data-label', 'datasetSettings.form.retrievalSetting.title')
|
||||
})
|
||||
|
||||
it('should show semantic search title for qualified indexing with semantic method', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[2]).toHaveTextContent('dataset.retrieval.semantic_search.title')
|
||||
})
|
||||
|
||||
it('should show full text search title for fullText method', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.fullText}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[2]).toHaveTextContent('dataset.retrieval.full_text_search.title')
|
||||
})
|
||||
|
||||
it('should show hybrid search title for hybrid method', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.hybrid}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[2]).toHaveTextContent('dataset.retrieval.hybrid_search.title')
|
||||
})
|
||||
|
||||
it('should force keyword_search for economical indexing type', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.ECONOMICAL}
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[2]).toHaveTextContent('dataset.retrieval.keyword_search.title')
|
||||
})
|
||||
|
||||
it('should show vector icon for semantic search', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/vector.svg')
|
||||
})
|
||||
|
||||
it('should show fullText icon for full text search', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.fullText}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/fullText.svg')
|
||||
})
|
||||
|
||||
it('should show hybrid icon for hybrid search', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.hybrid}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/hybrid.svg')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle all props undefined', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getAllByTestId('field-info')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should handle undefined indexingType with defined retrievalMethod', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail retrievalMethod={RETRIEVE_METHOD.hybrid} />)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
// When indexingType is undefined, it's treated as qualified
|
||||
expect(fieldValues[1]).toHaveTextContent('datasetCreation.stepTwo.qualified')
|
||||
})
|
||||
|
||||
it('should handle undefined retrievalMethod with defined indexingType', () => {
|
||||
// Arrange & Act
|
||||
render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
|
||||
// Assert
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
// When retrievalMethod is undefined, vector icon is used as default
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/vector.svg')
|
||||
})
|
||||
|
||||
it('should handle sourceData with null rules', () => {
|
||||
// Arrange
|
||||
const sourceData = {
|
||||
...createMockProcessRule(),
|
||||
mode: ProcessMode.parentChild,
|
||||
rules: null as unknown as ProcessRuleResponse['rules'],
|
||||
}
|
||||
|
||||
// Act & Assert - should not crash
|
||||
render(<RuleDetail sourceData={sourceData} />)
|
||||
expect(screen.getAllByTestId('field-info')).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Variations Tests
|
||||
// ==========================================
|
||||
describe('Props Variations', () => {
|
||||
it('should render correctly with all props provided', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule({ mode: ProcessMode.general })
|
||||
|
||||
// Act
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={sourceData}
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[0]).toHaveTextContent('datasetDocuments.embedding.custom')
|
||||
expect(fieldValues[1]).toHaveTextContent('datasetCreation.stepTwo.qualified')
|
||||
expect(fieldValues[2]).toHaveTextContent('dataset.retrieval.semantic_search.title')
|
||||
})
|
||||
|
||||
it('should render correctly for economical mode with full settings', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule({ mode: ProcessMode.parentChild })
|
||||
|
||||
// Act
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={sourceData}
|
||||
indexingType={IndexingType.ECONOMICAL}
|
||||
retrievalMethod={RETRIEVE_METHOD.fullText}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const fieldValues = screen.getAllByTestId('field-value')
|
||||
expect(fieldValues[1]).toHaveTextContent('datasetCreation.stepTwo.economical')
|
||||
// Economical always uses keyword_search regardless of retrievalMethod
|
||||
expect(fieldValues[2]).toHaveTextContent('dataset.retrieval.keyword_search.title')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Memoization Tests
|
||||
// ==========================================
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped in React.memo', () => {
|
||||
// Assert - RuleDetail should be a memoized component
|
||||
expect(RuleDetail).toHaveProperty('$$typeof', Symbol.for('react.memo'))
|
||||
})
|
||||
|
||||
it('should not re-render with same props', () => {
|
||||
// Arrange
|
||||
const sourceData = createMockProcessRule()
|
||||
const props = {
|
||||
sourceData,
|
||||
indexingType: IndexingType.QUALIFIED,
|
||||
retrievalMethod: RETRIEVE_METHOD.semantic,
|
||||
}
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<RuleDetail {...props} />)
|
||||
rerender(<RuleDetail {...props} />)
|
||||
|
||||
// Assert - component renders correctly after rerender
|
||||
expect(screen.getAllByTestId('field-info')).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -39,7 +39,7 @@ const RuleDetail = ({
|
||||
}, [sourceData, t])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-col gap-1' data-testid='rule-detail'>
|
||||
<FieldInfo
|
||||
label={t('datasetDocuments.embedding.mode')}
|
||||
displayedValue={getValue('mode')}
|
||||
|
||||
@@ -0,0 +1,808 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import Processing from './index'
|
||||
import type { InitialDocumentDetail } from '@/models/pipeline'
|
||||
import { DatasourceType } from '@/models/pipeline'
|
||||
import type { DocumentIndexingStatus } from '@/models/datasets'
|
||||
|
||||
// ==========================================
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock react-i18next (handled by __mocks__/react-i18next.ts but we override for custom messages)
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useDocLink - returns a function that generates doc URLs
|
||||
// Strips leading slash from path to match actual implementation behavior
|
||||
jest.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path?: string) => {
|
||||
const normalizedPath = path?.startsWith('/') ? path.slice(1) : (path || '')
|
||||
return `https://docs.dify.ai/en-US/${normalizedPath}`
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock dataset detail context
|
||||
let mockDataset: {
|
||||
id?: string
|
||||
indexing_technique?: string
|
||||
retrieval_model_dict?: { search_method?: string }
|
||||
} | undefined
|
||||
|
||||
jest.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: <T,>(selector: (state: { dataset?: typeof mockDataset }) => T): T => {
|
||||
return selector({ dataset: mockDataset })
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the EmbeddingProcess component to track props
|
||||
let embeddingProcessProps: Record<string, unknown> = {}
|
||||
jest.mock('./embedding-process', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, unknown>) => {
|
||||
embeddingProcessProps = props
|
||||
return (
|
||||
<div data-testid="embedding-process">
|
||||
<span data-testid="ep-dataset-id">{props.datasetId as string}</span>
|
||||
<span data-testid="ep-batch-id">{props.batchId as string}</span>
|
||||
<span data-testid="ep-documents-count">{(props.documents as unknown[])?.length ?? 0}</span>
|
||||
<span data-testid="ep-indexing-type">{props.indexingType as string || 'undefined'}</span>
|
||||
<span data-testid="ep-retrieval-method">{props.retrievalMethod as string || 'undefined'}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// ==========================================
|
||||
// Test Data Factory Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Creates a mock InitialDocumentDetail for testing
|
||||
* Uses deterministic counter-based IDs to avoid flaky tests
|
||||
*/
|
||||
let documentIdCounter = 0
|
||||
const createMockDocument = (overrides: Partial<InitialDocumentDetail> = {}): InitialDocumentDetail => ({
|
||||
id: overrides.id ?? `doc-${++documentIdCounter}`,
|
||||
name: 'test-document.txt',
|
||||
data_source_type: DatasourceType.localFile,
|
||||
data_source_info: {},
|
||||
enable: true,
|
||||
error: '',
|
||||
indexing_status: 'waiting' as DocumentIndexingStatus,
|
||||
position: 0,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a list of mock documents
|
||||
*/
|
||||
const createMockDocuments = (count: number): InitialDocumentDetail[] =>
|
||||
Array.from({ length: count }, (_, index) =>
|
||||
createMockDocument({
|
||||
id: `doc-${index + 1}`,
|
||||
name: `document-${index + 1}.txt`,
|
||||
position: index,
|
||||
}),
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// Test Suite
|
||||
// ==========================================
|
||||
|
||||
describe('Processing', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
embeddingProcessProps = {}
|
||||
// Reset deterministic ID counter for reproducible tests
|
||||
documentIdCounter = 0
|
||||
// Reset mock dataset with default values
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
// Tests basic rendering functionality
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(2),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the EmbeddingProcess component', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-456',
|
||||
documents: createMockDocuments(3),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the side tip section with correct content', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert - verify translation keys are rendered
|
||||
expect(screen.getByText('datasetCreation.stepThree.sideTipTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepThree.sideTipContent')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the documentation link with correct attributes', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link', { name: 'datasetPipeline.addDocuments.stepThree.learnMore' })
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/guides/knowledge-base/integrate-knowledge-within-application')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noreferrer noopener')
|
||||
})
|
||||
|
||||
it('should render the book icon in the side tip', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = render(<Processing {...props} />)
|
||||
|
||||
// Assert - check for icon container with shadow styling
|
||||
const iconContainer = container.querySelector('.shadow-lg.shadow-shadow-shadow-5')
|
||||
expect(iconContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
// Tests that props are correctly passed to child components
|
||||
it('should pass batchId to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
const testBatchId = 'test-batch-id-789'
|
||||
const props = {
|
||||
batchId: testBatchId,
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent(testBatchId)
|
||||
expect(embeddingProcessProps.batchId).toBe(testBatchId)
|
||||
})
|
||||
|
||||
it('should pass documents to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
const documents = createMockDocuments(5)
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('5')
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
|
||||
it('should pass datasetId from context to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'context-dataset-id',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('context-dataset-id')
|
||||
expect(embeddingProcessProps.datasetId).toBe('context-dataset-id')
|
||||
})
|
||||
|
||||
it('should pass indexingType from context to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: 'economy',
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-indexing-type')).toHaveTextContent('economy')
|
||||
expect(embeddingProcessProps.indexingType).toBe('economy')
|
||||
})
|
||||
|
||||
it('should pass retrievalMethod from context to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: 'keyword_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-retrieval-method')).toHaveTextContent('keyword_search')
|
||||
expect(embeddingProcessProps.retrievalMethod).toBe('keyword_search')
|
||||
})
|
||||
|
||||
it('should handle different document types', () => {
|
||||
// Arrange
|
||||
const documents = [
|
||||
createMockDocument({
|
||||
id: 'doc-local',
|
||||
name: 'local-file.pdf',
|
||||
data_source_type: DatasourceType.localFile,
|
||||
}),
|
||||
createMockDocument({
|
||||
id: 'doc-online',
|
||||
name: 'online-doc',
|
||||
data_source_type: DatasourceType.onlineDocument,
|
||||
}),
|
||||
createMockDocument({
|
||||
id: 'doc-website',
|
||||
name: 'website-page',
|
||||
data_source_type: DatasourceType.websiteCrawl,
|
||||
}),
|
||||
]
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('3')
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
// Tests for boundary conditions and unusual inputs
|
||||
it('should handle empty documents array', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: [],
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('0')
|
||||
expect(embeddingProcessProps.documents).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle empty batchId', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: '',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle undefined dataset from context', () => {
|
||||
// Arrange
|
||||
mockDataset = undefined
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.datasetId).toBeUndefined()
|
||||
expect(embeddingProcessProps.indexingType).toBeUndefined()
|
||||
expect(embeddingProcessProps.retrievalMethod).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle dataset with undefined id', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: undefined,
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.datasetId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle dataset with undefined indexing_technique', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: undefined,
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.indexingType).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle dataset with undefined retrieval_model_dict', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: undefined,
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.retrievalMethod).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle dataset with empty retrieval_model_dict', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: {},
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.retrievalMethod).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle large number of documents', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(100),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('100')
|
||||
})
|
||||
|
||||
it('should handle documents with error status', () => {
|
||||
// Arrange
|
||||
const documents = [
|
||||
createMockDocument({
|
||||
id: 'doc-error',
|
||||
name: 'error-doc.txt',
|
||||
error: 'Processing failed',
|
||||
indexing_status: 'error' as DocumentIndexingStatus,
|
||||
}),
|
||||
]
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
|
||||
it('should handle documents with special characters in names', () => {
|
||||
// Arrange
|
||||
const documents = [
|
||||
createMockDocument({
|
||||
id: 'doc-special',
|
||||
name: 'document with spaces & special-chars_测试.pdf',
|
||||
}),
|
||||
]
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
|
||||
it('should handle batchId with special characters', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123-abc_xyz:456',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('batch-123-abc_xyz:456')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Context Integration Tests
|
||||
// ==========================================
|
||||
describe('Context Integration', () => {
|
||||
// Tests for proper context usage
|
||||
it('should correctly use context selectors for all dataset properties', () => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'full-dataset-id',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: 'hybrid_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(embeddingProcessProps.datasetId).toBe('full-dataset-id')
|
||||
expect(embeddingProcessProps.indexingType).toBe('high_quality')
|
||||
expect(embeddingProcessProps.retrievalMethod).toBe('hybrid_search')
|
||||
})
|
||||
|
||||
it('should handle context changes with different indexing techniques', () => {
|
||||
// Arrange - Test with economy indexing
|
||||
mockDataset = {
|
||||
id: 'dataset-economy',
|
||||
indexing_technique: 'economy',
|
||||
retrieval_model_dict: { search_method: 'keyword_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<Processing {...props} />)
|
||||
|
||||
// Assert economy indexing
|
||||
expect(embeddingProcessProps.indexingType).toBe('economy')
|
||||
|
||||
// Arrange - Update to high_quality
|
||||
mockDataset = {
|
||||
id: 'dataset-hq',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
|
||||
// Act - Rerender with new context
|
||||
rerender(<Processing {...props} />)
|
||||
|
||||
// Assert high_quality indexing
|
||||
expect(embeddingProcessProps.indexingType).toBe('high_quality')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Layout Tests
|
||||
// ==========================================
|
||||
describe('Layout', () => {
|
||||
// Tests for proper layout and structure
|
||||
it('should render with correct layout structure', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = render(<Processing {...props} />)
|
||||
|
||||
// Assert - Check for flex layout with proper widths
|
||||
const mainContainer = container.querySelector('.flex.h-full.w-full.justify-center')
|
||||
expect(mainContainer).toBeInTheDocument()
|
||||
|
||||
// Check for left panel (3/5 width)
|
||||
const leftPanel = container.querySelector('.w-3\\/5')
|
||||
expect(leftPanel).toBeInTheDocument()
|
||||
|
||||
// Check for right panel (2/5 width)
|
||||
const rightPanel = container.querySelector('.w-2\\/5')
|
||||
expect(rightPanel).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render side tip card with correct styling', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = render(<Processing {...props} />)
|
||||
|
||||
// Assert - Check for card container with rounded corners and background
|
||||
const sideTipCard = container.querySelector('.rounded-xl.bg-background-section')
|
||||
expect(sideTipCard).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should constrain max-width for EmbeddingProcess container', () => {
|
||||
// Arrange
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
const maxWidthContainer = container.querySelector('.max-w-\\[640px\\]')
|
||||
expect(maxWidthContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Document Variations Tests
|
||||
// ==========================================
|
||||
describe('Document Variations', () => {
|
||||
// Tests for different document configurations
|
||||
it('should handle documents with all indexing statuses', () => {
|
||||
// Arrange
|
||||
const statuses: DocumentIndexingStatus[] = [
|
||||
'waiting',
|
||||
'parsing',
|
||||
'cleaning',
|
||||
'splitting',
|
||||
'indexing',
|
||||
'paused',
|
||||
'error',
|
||||
'completed',
|
||||
]
|
||||
const documents = statuses.map((status, index) =>
|
||||
createMockDocument({
|
||||
id: `doc-${status}`,
|
||||
name: `${status}-doc.txt`,
|
||||
indexing_status: status,
|
||||
position: index,
|
||||
}),
|
||||
)
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent(String(statuses.length))
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
|
||||
it('should handle documents with enabled and disabled states', () => {
|
||||
// Arrange
|
||||
const documents = [
|
||||
createMockDocument({ id: 'doc-enabled', enable: true }),
|
||||
createMockDocument({ id: 'doc-disabled', enable: false }),
|
||||
]
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('2')
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
|
||||
it('should handle documents from online drive source', () => {
|
||||
// Arrange
|
||||
const documents = [
|
||||
createMockDocument({
|
||||
id: 'doc-drive',
|
||||
name: 'google-drive-doc',
|
||||
data_source_type: DatasourceType.onlineDrive,
|
||||
data_source_info: { provider: 'google_drive' },
|
||||
}),
|
||||
]
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
|
||||
it('should handle documents with complex data_source_info', () => {
|
||||
// Arrange
|
||||
const documents = [
|
||||
createMockDocument({
|
||||
id: 'doc-notion',
|
||||
name: 'Notion Page',
|
||||
data_source_type: DatasourceType.onlineDocument,
|
||||
data_source_info: {
|
||||
notion_page_icon: { type: 'emoji', emoji: '📄' },
|
||||
notion_workspace_id: 'ws-123',
|
||||
notion_page_id: 'page-456',
|
||||
},
|
||||
}),
|
||||
]
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(embeddingProcessProps.documents).toEqual(documents)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Retrieval Method Variations
|
||||
// ==========================================
|
||||
describe('Retrieval Method Variations', () => {
|
||||
// Tests for different retrieval methods
|
||||
const retrievalMethods = ['semantic_search', 'keyword_search', 'hybrid_search', 'full_text_search']
|
||||
|
||||
it.each(retrievalMethods)('should handle %s retrieval method', (method) => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: { search_method: method },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(embeddingProcessProps.retrievalMethod).toBe(method)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Indexing Technique Variations
|
||||
// ==========================================
|
||||
describe('Indexing Technique Variations', () => {
|
||||
// Tests for different indexing techniques
|
||||
const indexingTechniques = ['high_quality', 'economy']
|
||||
|
||||
it.each(indexingTechniques)('should handle %s indexing technique', (technique) => {
|
||||
// Arrange
|
||||
mockDataset = {
|
||||
id: 'dataset-123',
|
||||
indexing_technique: technique,
|
||||
retrieval_model_dict: { search_method: 'semantic_search' },
|
||||
}
|
||||
const props = {
|
||||
batchId: 'batch-123',
|
||||
documents: createMockDocuments(1),
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<Processing {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(embeddingProcessProps.indexingType).toBe(technique)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -20,7 +20,7 @@ const Processing = ({
|
||||
const docLink = useDocLink()
|
||||
const datasetId = useDatasetDetailContextWithSelector(s => s.dataset?.id)
|
||||
const indexingType = useDatasetDetailContextWithSelector(s => s.dataset?.indexing_technique)
|
||||
const retrievalMethod = useDatasetDetailContextWithSelector(s => s.dataset?.retrieval_model_dict.search_method)
|
||||
const retrievalMethod = useDatasetDetailContextWithSelector(s => s.dataset?.retrieval_model_dict?.search_method)
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-full justify-center overflow-hidden'>
|
||||
|
||||
Reference in New Issue
Block a user