import type { CrawlResultItem } from '@/models/datasets' import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import CrawledResult from './base/crawled-result' import CrawledResultItem from './base/crawled-result-item' import Header from './base/header' import Input from './base/input' // ============================================================================ // Test Data Factories // ============================================================================ const createCrawlResultItem = (overrides: Partial = {}): CrawlResultItem => ({ title: 'Test Page Title', markdown: '# Test Content', description: 'Test description', source_url: 'https://example.com/page', ...overrides, }) // ============================================================================ // Input Component Tests // ============================================================================ describe('Input', () => { beforeEach(() => { vi.clearAllMocks() }) const createInputProps = (overrides: Partial[0]> = {}) => ({ value: '', onChange: vi.fn(), ...overrides, }) describe('Rendering', () => { it('should render text input by default', () => { const props = createInputProps() render() const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() expect(input).toHaveAttribute('type', 'text') }) it('should render number input when isNumber is true', () => { const props = createInputProps({ isNumber: true, value: 0 }) render() const input = screen.getByRole('spinbutton') expect(input).toBeInTheDocument() expect(input).toHaveAttribute('type', 'number') expect(input).toHaveAttribute('min', '0') }) it('should render with placeholder', () => { const props = createInputProps({ placeholder: 'Enter URL' }) render() expect(screen.getByPlaceholderText('Enter URL')).toBeInTheDocument() }) it('should render with initial value', () => { const props = createInputProps({ value: 'test value' }) render() expect(screen.getByDisplayValue('test value')).toBeInTheDocument() }) }) describe('Text Input Behavior', () => { it('should call onChange with string value for text input', async () => { const onChange = vi.fn() const props = createInputProps({ onChange }) render() const input = screen.getByRole('textbox') await userEvent.type(input, 'hello') expect(onChange).toHaveBeenCalledWith('h') expect(onChange).toHaveBeenCalledWith('e') expect(onChange).toHaveBeenCalledWith('l') expect(onChange).toHaveBeenCalledWith('l') expect(onChange).toHaveBeenCalledWith('o') }) }) describe('Number Input Behavior', () => { it('should call onChange with parsed integer for number input', () => { const onChange = vi.fn() const props = createInputProps({ isNumber: true, onChange, value: 0 }) render() const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: '42' } }) expect(onChange).toHaveBeenCalledWith(42) }) it('should call onChange with empty string when input is NaN', () => { const onChange = vi.fn() const props = createInputProps({ isNumber: true, onChange, value: 0 }) render() const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: 'abc' } }) expect(onChange).toHaveBeenCalledWith('') }) it('should call onChange with empty string when input is empty', () => { const onChange = vi.fn() const props = createInputProps({ isNumber: true, onChange, value: 5 }) render() const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: '' } }) expect(onChange).toHaveBeenCalledWith('') }) it('should clamp negative values to MIN_VALUE (0)', () => { const onChange = vi.fn() const props = createInputProps({ isNumber: true, onChange, value: 0 }) render() const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: '-5' } }) expect(onChange).toHaveBeenCalledWith(0) }) it('should handle decimal input by parsing as integer', () => { const onChange = vi.fn() const props = createInputProps({ isNumber: true, onChange, value: 0 }) render() const input = screen.getByRole('spinbutton') fireEvent.change(input, { target: { value: '3.7' } }) expect(onChange).toHaveBeenCalledWith(3) }) }) describe('Component Memoization', () => { it('should be wrapped with React.memo', () => { expect(Input.$$typeof).toBeDefined() }) }) }) // ============================================================================ // Header Component Tests // ============================================================================ describe('Header', () => { const createHeaderProps = (overrides: Partial[0]> = {}) => ({ title: 'Test Title', docTitle: 'Documentation', docLink: 'https://docs.example.com', ...overrides, }) describe('Rendering', () => { it('should render title', () => { const props = createHeaderProps() render(
) expect(screen.getByText('Test Title')).toBeInTheDocument() }) it('should render doc link', () => { const props = createHeaderProps() render(
) const link = screen.getByRole('link') expect(link).toHaveAttribute('href', 'https://docs.example.com') expect(link).toHaveAttribute('target', '_blank') }) it('should render button text when not in pipeline', () => { const props = createHeaderProps({ buttonText: 'Configure' }) render(
) expect(screen.getByText('Configure')).toBeInTheDocument() }) it('should not render button text when in pipeline', () => { const props = createHeaderProps({ isInPipeline: true, buttonText: 'Configure' }) render(
) expect(screen.queryByText('Configure')).not.toBeInTheDocument() }) }) describe('isInPipeline Prop', () => { it('should apply pipeline styles when isInPipeline is true', () => { const props = createHeaderProps({ isInPipeline: true }) render(
) const titleElement = screen.getByText('Test Title') expect(titleElement).toHaveClass('system-sm-semibold') }) it('should apply default styles when isInPipeline is false', () => { const props = createHeaderProps({ isInPipeline: false }) render(
) const titleElement = screen.getByText('Test Title') expect(titleElement).toHaveClass('system-md-semibold') }) it('should apply compact button styles when isInPipeline is true', () => { const props = createHeaderProps({ isInPipeline: true }) render(
) const button = screen.getByRole('button') expect(button).toHaveClass('size-6') expect(button).toHaveClass('px-1') }) it('should apply default button styles when isInPipeline is false', () => { const props = createHeaderProps({ isInPipeline: false }) render(
) const button = screen.getByRole('button') expect(button).toHaveClass('gap-x-0.5') expect(button).toHaveClass('px-1.5') }) }) describe('User Interactions', () => { it('should call onClickConfiguration when button is clicked', async () => { const onClickConfiguration = vi.fn() const props = createHeaderProps({ onClickConfiguration }) render(
) await userEvent.click(screen.getByRole('button')) expect(onClickConfiguration).toHaveBeenCalledTimes(1) }) }) describe('Component Memoization', () => { it('should be wrapped with React.memo', () => { expect(Header.$$typeof).toBeDefined() }) }) }) // ============================================================================ // CrawledResultItem Component Tests // ============================================================================ describe('CrawledResultItem', () => { const createItemProps = (overrides: Partial[0]> = {}) => ({ payload: createCrawlResultItem(), isChecked: false, isPreview: false, onCheckChange: vi.fn(), onPreview: vi.fn(), testId: 'test-item', ...overrides, }) describe('Rendering', () => { it('should render title and source URL', () => { const props = createItemProps({ payload: createCrawlResultItem({ title: 'My Page', source_url: 'https://mysite.com', }), }) render() expect(screen.getByText('My Page')).toBeInTheDocument() expect(screen.getByText('https://mysite.com')).toBeInTheDocument() }) it('should render checkbox (custom Checkbox component)', () => { const props = createItemProps() render() // Find checkbox by data-testid const checkbox = screen.getByTestId('checkbox-test-item') expect(checkbox).toBeInTheDocument() }) it('should render preview button', () => { const props = createItemProps() render() expect(screen.getByText('datasetCreation.stepOne.website.preview')).toBeInTheDocument() }) }) describe('Checkbox Behavior', () => { it('should call onCheckChange with true when unchecked item is clicked', async () => { const onCheckChange = vi.fn() const props = createItemProps({ isChecked: false, onCheckChange }) render() const checkbox = screen.getByTestId('checkbox-test-item') await userEvent.click(checkbox) expect(onCheckChange).toHaveBeenCalledWith(true) }) it('should call onCheckChange with false when checked item is clicked', async () => { const onCheckChange = vi.fn() const props = createItemProps({ isChecked: true, onCheckChange }) render() const checkbox = screen.getByTestId('checkbox-test-item') await userEvent.click(checkbox) expect(onCheckChange).toHaveBeenCalledWith(false) }) }) describe('Preview Behavior', () => { it('should call onPreview when preview button is clicked', async () => { const onPreview = vi.fn() const props = createItemProps({ onPreview }) render() await userEvent.click(screen.getByText('datasetCreation.stepOne.website.preview')) expect(onPreview).toHaveBeenCalledTimes(1) }) it('should apply active style when isPreview is true', () => { const props = createItemProps({ isPreview: true }) const { container } = render() const wrapper = container.firstChild expect(wrapper).toHaveClass('bg-state-base-active') }) it('should not apply active style when isPreview is false', () => { const props = createItemProps({ isPreview: false }) const { container } = render() const wrapper = container.firstChild expect(wrapper).not.toHaveClass('bg-state-base-active') }) }) describe('Component Memoization', () => { it('should be wrapped with React.memo', () => { expect(CrawledResultItem.$$typeof).toBeDefined() }) }) }) // ============================================================================ // CrawledResult Component Tests // ============================================================================ describe('CrawledResult', () => { const createResultProps = (overrides: Partial[0]> = {}) => ({ list: [ createCrawlResultItem({ source_url: 'https://page1.com', title: 'Page 1' }), createCrawlResultItem({ source_url: 'https://page2.com', title: 'Page 2' }), createCrawlResultItem({ source_url: 'https://page3.com', title: 'Page 3' }), ], checkedList: [], onSelectedChange: vi.fn(), onPreview: vi.fn(), usedTime: 2.5, ...overrides, }) // Helper functions to get checkboxes by data-testid const getSelectAllCheckbox = () => screen.getByTestId('checkbox-select-all') const getItemCheckbox = (index: number) => screen.getByTestId(`checkbox-item-${index}`) describe('Rendering', () => { it('should render all items in list', () => { const props = createResultProps() render() expect(screen.getByText('Page 1')).toBeInTheDocument() expect(screen.getByText('Page 2')).toBeInTheDocument() expect(screen.getByText('Page 3')).toBeInTheDocument() }) it('should render time info', () => { const props = createResultProps({ usedTime: 3.456 }) render() // The component uses i18n, so we check for the key pattern expect(screen.getByText(/scrapTimeInfo/)).toBeInTheDocument() }) it('should render select all checkbox', () => { const props = createResultProps() render() expect(screen.getByText('datasetCreation.stepOne.website.selectAll')).toBeInTheDocument() }) it('should render reset all when all items are checked', () => { const list = [ createCrawlResultItem({ source_url: 'https://page1.com' }), createCrawlResultItem({ source_url: 'https://page2.com' }), ] const props = createResultProps({ list, checkedList: list }) render() expect(screen.getByText('datasetCreation.stepOne.website.resetAll')).toBeInTheDocument() }) }) describe('Select All / Deselect All', () => { it('should call onSelectedChange with all items when select all is clicked', async () => { const onSelectedChange = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com' }), createCrawlResultItem({ source_url: 'https://page2.com' }), ] const props = createResultProps({ list, checkedList: [], onSelectedChange }) render() await userEvent.click(getSelectAllCheckbox()) expect(onSelectedChange).toHaveBeenCalledWith(list) }) it('should call onSelectedChange with empty array when reset all is clicked', async () => { const onSelectedChange = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com' }), createCrawlResultItem({ source_url: 'https://page2.com' }), ] const props = createResultProps({ list, checkedList: list, onSelectedChange }) render() await userEvent.click(getSelectAllCheckbox()) expect(onSelectedChange).toHaveBeenCalledWith([]) }) }) describe('Individual Item Selection', () => { it('should add item to checkedList when unchecked item is checked', async () => { const onSelectedChange = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com', title: 'Page 1' }), createCrawlResultItem({ source_url: 'https://page2.com', title: 'Page 2' }), ] const props = createResultProps({ list, checkedList: [], onSelectedChange }) render() await userEvent.click(getItemCheckbox(0)) expect(onSelectedChange).toHaveBeenCalledWith([list[0]]) }) it('should remove item from checkedList when checked item is unchecked', async () => { const onSelectedChange = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com', title: 'Page 1' }), createCrawlResultItem({ source_url: 'https://page2.com', title: 'Page 2' }), ] const props = createResultProps({ list, checkedList: [list[0]], onSelectedChange }) render() await userEvent.click(getItemCheckbox(0)) expect(onSelectedChange).toHaveBeenCalledWith([]) }) it('should preserve other checked items when unchecking one item', async () => { const onSelectedChange = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com', title: 'Page 1' }), createCrawlResultItem({ source_url: 'https://page2.com', title: 'Page 2' }), createCrawlResultItem({ source_url: 'https://page3.com', title: 'Page 3' }), ] const props = createResultProps({ list, checkedList: [list[0], list[1]], onSelectedChange }) render() // Click the first item's checkbox to uncheck it await userEvent.click(getItemCheckbox(0)) expect(onSelectedChange).toHaveBeenCalledWith([list[1]]) }) }) describe('Preview Behavior', () => { it('should call onPreview with correct item when preview is clicked', async () => { const onPreview = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com', title: 'Page 1' }), createCrawlResultItem({ source_url: 'https://page2.com', title: 'Page 2' }), ] const props = createResultProps({ list, onPreview }) render() // Click preview on second item const previewButtons = screen.getAllByText('datasetCreation.stepOne.website.preview') await userEvent.click(previewButtons[1]) expect(onPreview).toHaveBeenCalledWith(list[1]) }) it('should track preview index correctly', async () => { const onPreview = vi.fn() const list = [ createCrawlResultItem({ source_url: 'https://page1.com', title: 'Page 1' }), createCrawlResultItem({ source_url: 'https://page2.com', title: 'Page 2' }), ] const props = createResultProps({ list, onPreview }) render() // Click preview on first item const previewButtons = screen.getAllByText('datasetCreation.stepOne.website.preview') await userEvent.click(previewButtons[0]) expect(onPreview).toHaveBeenCalledWith(list[0]) }) }) describe('Component Memoization', () => { it('should be wrapped with React.memo', () => { expect(CrawledResult.$$typeof).toBeDefined() }) }) describe('Edge Cases', () => { it('should handle empty list', () => { const props = createResultProps({ list: [], checkedList: [] }) render() // Should still render the header with resetAll (empty list = all checked) expect(screen.getByText('datasetCreation.stepOne.website.resetAll')).toBeInTheDocument() }) it('should handle className prop', () => { const props = createResultProps({ className: 'custom-class' }) const { container } = render() expect(container.firstChild).toHaveClass('custom-class') }) }) })