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 => ({ 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() // 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() // 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( , ) const rootElement = container.firstChild as HTMLElement expect(rootElement).toHaveClass('custom-class') }) it('should render limit field with required indicator', () => { const payload = createMockCrawlOptions() render() // Limit field should have required indicator (*) const requiredIndicator = screen.getByText('*') expect(requiredIndicator).toBeInTheDocument() }) it('should render placeholder for excludes field', () => { const payload = createMockCrawlOptions() render() const excludesInput = screen.getByPlaceholderText('blog/*, /about/*') expect(excludesInput).toBeInTheDocument() }) it('should render placeholder for includes field', () => { const payload = createMockCrawlOptions() render() const includesInput = screen.getByPlaceholderText('articles/*') expect(includesInput).toBeInTheDocument() }) it('should render two checkboxes', () => { const payload = createMockCrawlOptions() const { container } = render() 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() 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() 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() 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() 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() const limitInput = screen.getByDisplayValue('25') expect(limitInput).toBeInTheDocument() }) it('should display max_depth value in input', () => { const payload = createMockCrawlOptions({ max_depth: 5 }) render() const maxDepthInput = screen.getByDisplayValue('5') expect(maxDepthInput).toBeInTheDocument() }) it('should display excludes value in input', () => { const payload = createMockCrawlOptions({ excludes: 'test/*' }) render() const excludesInput = screen.getByDisplayValue('test/*') expect(excludesInput).toBeInTheDocument() }) it('should display includes value in input', () => { const payload = createMockCrawlOptions({ includes: 'docs/*' }) render() 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() 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() 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() 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() 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() 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() 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() // 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() // 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() 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() 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() 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() // 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() 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() rerender() 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() expect(screen.getByDisplayValue('10')).toBeInTheDocument() rerender() expect(screen.getByDisplayValue('20')).toBeInTheDocument() }) }) })