mirror of https://github.com/langgenius/dify.git
406 lines
15 KiB
TypeScript
406 lines
15 KiB
TypeScript
import type { CrawlOptions } from '@/models/datasets'
|
|
import { fireEvent, render, screen } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import Options from './options'
|
|
|
|
// ============================================================================
|
|
// Test Data Factory
|
|
// ============================================================================
|
|
|
|
const createMockCrawlOptions = (overrides: Partial<CrawlOptions> = {}): CrawlOptions => ({
|
|
crawl_sub_pages: true,
|
|
limit: 10,
|
|
max_depth: 2,
|
|
excludes: '',
|
|
includes: '',
|
|
only_main_content: false,
|
|
use_sitemap: false,
|
|
...overrides,
|
|
})
|
|
|
|
// ============================================================================
|
|
// Options Component Tests
|
|
// ============================================================================
|
|
|
|
describe('Options', () => {
|
|
const mockOnChange = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// Helper to get checkboxes by test id pattern
|
|
const getCheckboxes = (container: HTMLElement) => {
|
|
return container.querySelectorAll('[data-testid^="checkbox-"]')
|
|
}
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Rendering Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
const payload = createMockCrawlOptions()
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
// Check that key elements are rendered
|
|
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/limit/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/maxDepth/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render all form fields', () => {
|
|
const payload = createMockCrawlOptions()
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
// Checkboxes
|
|
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/extractOnlyMainContent/i)).toBeInTheDocument()
|
|
|
|
// Text/Number fields
|
|
expect(screen.getByText(/limit/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/maxDepth/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/excludePaths/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/includeOnlyPaths/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render with custom className', () => {
|
|
const payload = createMockCrawlOptions()
|
|
const { container } = render(
|
|
<Options payload={payload} onChange={mockOnChange} className="custom-class" />,
|
|
)
|
|
|
|
const rootElement = container.firstChild as HTMLElement
|
|
expect(rootElement).toHaveClass('custom-class')
|
|
})
|
|
|
|
it('should render limit field with required indicator', () => {
|
|
const payload = createMockCrawlOptions()
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
// Limit field should have required indicator (*)
|
|
const requiredIndicator = screen.getByText('*')
|
|
expect(requiredIndicator).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render placeholder for excludes field', () => {
|
|
const payload = createMockCrawlOptions()
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const excludesInput = screen.getByPlaceholderText('blog/*, /about/*')
|
|
expect(excludesInput).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render placeholder for includes field', () => {
|
|
const payload = createMockCrawlOptions()
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const includesInput = screen.getByPlaceholderText('articles/*')
|
|
expect(includesInput).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render two checkboxes', () => {
|
|
const payload = createMockCrawlOptions()
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
expect(checkboxes.length).toBe(2)
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Props Display Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Props Display', () => {
|
|
it('should display crawl_sub_pages checkbox with check icon when true', () => {
|
|
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
// First checkbox should have check icon when checked
|
|
expect(checkboxes[0].querySelector('svg')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display crawl_sub_pages checkbox without check icon when false', () => {
|
|
const payload = createMockCrawlOptions({ crawl_sub_pages: false })
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
// First checkbox should not have check icon when unchecked
|
|
expect(checkboxes[0].querySelector('svg')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should display only_main_content checkbox with check icon when true', () => {
|
|
const payload = createMockCrawlOptions({ only_main_content: true })
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
// Second checkbox should have check icon when checked
|
|
expect(checkboxes[1].querySelector('svg')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display only_main_content checkbox without check icon when false', () => {
|
|
const payload = createMockCrawlOptions({ only_main_content: false })
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
// Second checkbox should not have check icon when unchecked
|
|
expect(checkboxes[1].querySelector('svg')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should display limit value in input', () => {
|
|
const payload = createMockCrawlOptions({ limit: 25 })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const limitInput = screen.getByDisplayValue('25')
|
|
expect(limitInput).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display max_depth value in input', () => {
|
|
const payload = createMockCrawlOptions({ max_depth: 5 })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const maxDepthInput = screen.getByDisplayValue('5')
|
|
expect(maxDepthInput).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display excludes value in input', () => {
|
|
const payload = createMockCrawlOptions({ excludes: 'test/*' })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const excludesInput = screen.getByDisplayValue('test/*')
|
|
expect(excludesInput).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display includes value in input', () => {
|
|
const payload = createMockCrawlOptions({ includes: 'docs/*' })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const includesInput = screen.getByDisplayValue('docs/*')
|
|
expect(includesInput).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// User Interactions Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('User Interactions', () => {
|
|
it('should call onChange with updated crawl_sub_pages when checkbox is clicked', () => {
|
|
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
fireEvent.click(checkboxes[0])
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
...payload,
|
|
crawl_sub_pages: false,
|
|
})
|
|
})
|
|
|
|
it('should call onChange with updated only_main_content when checkbox is clicked', () => {
|
|
const payload = createMockCrawlOptions({ only_main_content: false })
|
|
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const checkboxes = getCheckboxes(container)
|
|
fireEvent.click(checkboxes[1])
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
...payload,
|
|
only_main_content: true,
|
|
})
|
|
})
|
|
|
|
it('should call onChange with updated limit when input changes', () => {
|
|
const payload = createMockCrawlOptions({ limit: 10 })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const limitInput = screen.getByDisplayValue('10')
|
|
fireEvent.change(limitInput, { target: { value: '50' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
...payload,
|
|
limit: 50,
|
|
})
|
|
})
|
|
|
|
it('should call onChange with updated max_depth when input changes', () => {
|
|
const payload = createMockCrawlOptions({ max_depth: 2 })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const maxDepthInput = screen.getByDisplayValue('2')
|
|
fireEvent.change(maxDepthInput, { target: { value: '10' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
...payload,
|
|
max_depth: 10,
|
|
})
|
|
})
|
|
|
|
it('should call onChange with updated excludes when input changes', () => {
|
|
const payload = createMockCrawlOptions({ excludes: '' })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const excludesInput = screen.getByPlaceholderText('blog/*, /about/*')
|
|
fireEvent.change(excludesInput, { target: { value: 'admin/*' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
...payload,
|
|
excludes: 'admin/*',
|
|
})
|
|
})
|
|
|
|
it('should call onChange with updated includes when input changes', () => {
|
|
const payload = createMockCrawlOptions({ includes: '' })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const includesInput = screen.getByPlaceholderText('articles/*')
|
|
fireEvent.change(includesInput, { target: { value: 'public/*' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
...payload,
|
|
includes: 'public/*',
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Edge Cases Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Edge Cases', () => {
|
|
it('should handle empty string values', () => {
|
|
const payload = createMockCrawlOptions({
|
|
limit: '',
|
|
max_depth: '',
|
|
excludes: '',
|
|
includes: '',
|
|
} as unknown as CrawlOptions)
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
// Component should render without crashing
|
|
expect(screen.getByText(/limit/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle zero values', () => {
|
|
const payload = createMockCrawlOptions({
|
|
limit: 0,
|
|
max_depth: 0,
|
|
})
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
// Zero values should be displayed
|
|
const zeroInputs = screen.getAllByDisplayValue('0')
|
|
expect(zeroInputs.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
|
|
it('should handle large numbers', () => {
|
|
const payload = createMockCrawlOptions({
|
|
limit: 9999,
|
|
max_depth: 100,
|
|
})
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
expect(screen.getByDisplayValue('9999')).toBeInTheDocument()
|
|
expect(screen.getByDisplayValue('100')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle special characters in text fields', () => {
|
|
const payload = createMockCrawlOptions({
|
|
excludes: 'path/*/file?query=1¶m=2',
|
|
includes: 'docs/**/*.md',
|
|
})
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
expect(screen.getByDisplayValue('path/*/file?query=1¶m=2')).toBeInTheDocument()
|
|
expect(screen.getByDisplayValue('docs/**/*.md')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should preserve other payload fields when updating one field', () => {
|
|
const payload = createMockCrawlOptions({
|
|
crawl_sub_pages: true,
|
|
limit: 10,
|
|
max_depth: 2,
|
|
excludes: 'test/*',
|
|
includes: 'docs/*',
|
|
only_main_content: true,
|
|
})
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const limitInput = screen.getByDisplayValue('10')
|
|
fireEvent.change(limitInput, { target: { value: '20' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith({
|
|
crawl_sub_pages: true,
|
|
limit: 20,
|
|
max_depth: 2,
|
|
excludes: 'test/*',
|
|
includes: 'docs/*',
|
|
only_main_content: true,
|
|
use_sitemap: false,
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// handleChange Callback Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('handleChange Callback', () => {
|
|
it('should create a new callback for each key', () => {
|
|
const payload = createMockCrawlOptions()
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
// Change limit
|
|
const limitInput = screen.getByDisplayValue('10')
|
|
fireEvent.change(limitInput, { target: { value: '15' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(
|
|
expect.objectContaining({ limit: 15 }),
|
|
)
|
|
|
|
// Change max_depth
|
|
const maxDepthInput = screen.getByDisplayValue('2')
|
|
fireEvent.change(maxDepthInput, { target: { value: '5' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledWith(
|
|
expect.objectContaining({ max_depth: 5 }),
|
|
)
|
|
})
|
|
|
|
it('should handle multiple rapid changes', () => {
|
|
const payload = createMockCrawlOptions({ limit: 10 })
|
|
render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
const limitInput = screen.getByDisplayValue('10')
|
|
fireEvent.change(limitInput, { target: { value: '11' } })
|
|
fireEvent.change(limitInput, { target: { value: '12' } })
|
|
fireEvent.change(limitInput, { target: { value: '13' } })
|
|
|
|
expect(mockOnChange).toHaveBeenCalledTimes(3)
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Memoization Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Memoization', () => {
|
|
it('should be memoized with React.memo', () => {
|
|
const payload = createMockCrawlOptions()
|
|
const { rerender } = render(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
rerender(<Options payload={payload} onChange={mockOnChange} />)
|
|
|
|
expect(screen.getByText(/limit/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should re-render when payload changes', () => {
|
|
const payload1 = createMockCrawlOptions({ limit: 10 })
|
|
const payload2 = createMockCrawlOptions({ limit: 20 })
|
|
|
|
const { rerender } = render(<Options payload={payload1} onChange={mockOnChange} />)
|
|
expect(screen.getByDisplayValue('10')).toBeInTheDocument()
|
|
|
|
rerender(<Options payload={payload2} onChange={mockOnChange} />)
|
|
expect(screen.getByDisplayValue('20')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|