Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox

# Conflicts:
#	api/uv.lock
#	web/app/components/apps/__tests__/app-card.spec.tsx
#	web/app/components/apps/__tests__/list.spec.tsx
#	web/app/components/datasets/create/__tests__/index.spec.tsx
#	web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx
#	web/app/components/plugins/readme-panel/__tests__/index.spec.tsx
#	web/app/components/rag-pipeline/__tests__/index.spec.tsx
#	web/app/components/rag-pipeline/hooks/__tests__/index.spec.ts
#	web/eslint-suppressions.json
This commit is contained in:
yyh
2026-02-13 15:17:52 +08:00
898 changed files with 58772 additions and 34358 deletions

View File

@ -0,0 +1,50 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CheckboxWithLabel from '../checkbox-with-label'
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked, onCheck }: { checked: boolean, onCheck: () => void }) => (
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} />
),
}))
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ popupContent }: { popupContent: string }) => <div data-testid="tooltip">{popupContent}</div>,
}))
describe('CheckboxWithLabel', () => {
const defaultProps = {
isChecked: false,
onChange: vi.fn(),
label: 'Test Label',
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should render label text', () => {
render(<CheckboxWithLabel {...defaultProps} />)
expect(screen.getByText('Test Label')).toBeInTheDocument()
})
it('should render checkbox', () => {
render(<CheckboxWithLabel {...defaultProps} />)
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
})
it('should render tooltip when provided', () => {
render(<CheckboxWithLabel {...defaultProps} tooltip="Help text" />)
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
})
it('should not render tooltip when not provided', () => {
render(<CheckboxWithLabel {...defaultProps} />)
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument()
})
it('should apply custom className', () => {
const { container } = render(<CheckboxWithLabel {...defaultProps} className="custom-cls" />)
expect(container.querySelector('.custom-cls')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,69 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CrawledResultItem from '../crawled-result-item'
vi.mock('@/app/components/base/button', () => ({
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
<button data-testid="preview-button" onClick={onClick}>{children}</button>
),
}))
vi.mock('@/app/components/base/checkbox', () => ({
default: ({ checked, onCheck }: { checked: boolean, onCheck: () => void }) => (
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} />
),
}))
vi.mock('@/app/components/base/radio/ui', () => ({
default: ({ isChecked, onCheck }: { isChecked: boolean, onCheck: () => void }) => (
<input type="radio" data-testid="radio" checked={isChecked} onChange={onCheck} />
),
}))
describe('CrawledResultItem', () => {
const defaultProps = {
payload: {
title: 'Test Page',
source_url: 'https://example.com/page',
markdown: '',
description: '',
} satisfies CrawlResultItemType,
isChecked: false,
onCheckChange: vi.fn(),
isPreview: false,
showPreview: true,
onPreview: vi.fn(),
isMultipleChoice: true,
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should render title and URL', () => {
render(<CrawledResultItem {...defaultProps} />)
expect(screen.getByText('Test Page')).toBeInTheDocument()
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
})
it('should render checkbox in multiple choice mode', () => {
render(<CrawledResultItem {...defaultProps} />)
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
})
it('should render radio in single choice mode', () => {
render(<CrawledResultItem {...defaultProps} isMultipleChoice={false} />)
expect(screen.getByTestId('radio')).toBeInTheDocument()
})
it('should show preview button when showPreview is true', () => {
render(<CrawledResultItem {...defaultProps} />)
expect(screen.getByTestId('preview-button')).toBeInTheDocument()
})
it('should not show preview button when showPreview is false', () => {
render(<CrawledResultItem {...defaultProps} showPreview={false} />)
expect(screen.queryByTestId('preview-button')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,214 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import CrawledResult from '../crawled-result'
vi.mock('../checkbox-with-label', () => ({
default: ({ isChecked, onChange, label }: { isChecked: boolean, onChange: () => void, label: string }) => (
<label>
<input
type="checkbox"
checked={isChecked}
onChange={onChange}
data-testid="check-all-checkbox"
/>
{label}
</label>
),
}))
vi.mock('../crawled-result-item', () => ({
default: ({
payload,
isChecked,
onCheckChange,
onPreview,
}: {
payload: CrawlResultItem
isChecked: boolean
onCheckChange: (checked: boolean) => void
onPreview: () => void
}) => (
<div data-testid={`crawled-item-${payload.source_url}`}>
<span data-testid="item-url">{payload.source_url}</span>
<button data-testid={`check-${payload.source_url}`} onClick={() => onCheckChange(!isChecked)}>
{isChecked ? 'uncheck' : 'check'}
</button>
<button data-testid={`preview-${payload.source_url}`} onClick={onPreview}>
preview
</button>
</div>
),
}))
const createItem = (url: string): CrawlResultItem => ({
source_url: url,
title: `Title for ${url}`,
markdown: `# ${url}`,
description: `Desc for ${url}`,
})
const defaultList: CrawlResultItem[] = [
createItem('https://example.com/a'),
createItem('https://example.com/b'),
createItem('https://example.com/c'),
]
describe('CrawledResult', () => {
const defaultProps = {
list: defaultList,
checkedList: [] as CrawlResultItem[],
onSelectedChange: vi.fn(),
usedTime: 12.345,
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render scrap time info with correct total and time', () => {
render(<CrawledResult {...defaultProps} />)
expect(
screen.getByText(/scrapTimeInfo/),
).toBeInTheDocument()
// The global i18n mock serialises params, so verify total and time appear
expect(screen.getByText(/"total":3/)).toBeInTheDocument()
expect(screen.getByText(/"time":"12.3"/)).toBeInTheDocument()
})
it('should render all items from list', () => {
render(<CrawledResult {...defaultProps} />)
for (const item of defaultList) {
expect(screen.getByTestId(`crawled-item-${item.source_url}`)).toBeInTheDocument()
}
})
it('should apply custom className', () => {
const { container } = render(
<CrawledResult {...defaultProps} className="my-custom-class" />,
)
expect(container.firstChild).toHaveClass('my-custom-class')
})
})
// Check-all checkbox visibility
describe('Check All Checkbox', () => {
it('should show check-all checkbox in multiple choice mode', () => {
render(<CrawledResult {...defaultProps} isMultipleChoice={true} />)
expect(screen.getByTestId('check-all-checkbox')).toBeInTheDocument()
})
it('should hide check-all checkbox in single choice mode', () => {
render(<CrawledResult {...defaultProps} isMultipleChoice={false} />)
expect(screen.queryByTestId('check-all-checkbox')).not.toBeInTheDocument()
})
})
// Toggle all items
describe('Toggle All', () => {
it('should select all when not all checked', () => {
const onSelectedChange = vi.fn()
render(
<CrawledResult
{...defaultProps}
checkedList={[defaultList[0]]}
onSelectedChange={onSelectedChange}
/>,
)
fireEvent.click(screen.getByTestId('check-all-checkbox'))
expect(onSelectedChange).toHaveBeenCalledWith(defaultList)
})
it('should deselect all when all checked', () => {
const onSelectedChange = vi.fn()
render(
<CrawledResult
{...defaultProps}
checkedList={[...defaultList]}
onSelectedChange={onSelectedChange}
/>,
)
fireEvent.click(screen.getByTestId('check-all-checkbox'))
expect(onSelectedChange).toHaveBeenCalledWith([])
})
})
// Individual item check
describe('Individual Item Check', () => {
it('should add item to selection in multiple choice mode', () => {
const onSelectedChange = vi.fn()
render(
<CrawledResult
{...defaultProps}
checkedList={[defaultList[0]]}
onSelectedChange={onSelectedChange}
isMultipleChoice={true}
/>,
)
fireEvent.click(screen.getByTestId(`check-${defaultList[1].source_url}`))
expect(onSelectedChange).toHaveBeenCalledWith([defaultList[0], defaultList[1]])
})
it('should replace selection in single choice mode', () => {
const onSelectedChange = vi.fn()
render(
<CrawledResult
{...defaultProps}
checkedList={[defaultList[0]]}
onSelectedChange={onSelectedChange}
isMultipleChoice={false}
/>,
)
fireEvent.click(screen.getByTestId(`check-${defaultList[1].source_url}`))
expect(onSelectedChange).toHaveBeenCalledWith([defaultList[1]])
})
it('should remove item from selection when unchecked', () => {
const onSelectedChange = vi.fn()
render(
<CrawledResult
{...defaultProps}
checkedList={[defaultList[0], defaultList[1]]}
onSelectedChange={onSelectedChange}
isMultipleChoice={true}
/>,
)
fireEvent.click(screen.getByTestId(`check-${defaultList[0].source_url}`))
expect(onSelectedChange).toHaveBeenCalledWith([defaultList[1]])
})
})
// Preview
describe('Preview', () => {
it('should call onPreview with correct item and index', () => {
const onPreview = vi.fn()
render(
<CrawledResult
{...defaultProps}
onPreview={onPreview}
showPreview={true}
/>,
)
fireEvent.click(screen.getByTestId(`preview-${defaultList[1].source_url}`))
expect(onPreview).toHaveBeenCalledWith(defaultList[1], 1)
})
})
})

View File

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Crawling from '../crawling'
describe('Crawling', () => {
it('should render crawl progress', () => {
render(<Crawling crawledNum={5} totalNum={10} />)
expect(screen.getByText(/5/)).toBeInTheDocument()
expect(screen.getByText(/10/)).toBeInTheDocument()
})
it('should render total page scraped label', () => {
render(<Crawling crawledNum={0} totalNum={0} />)
expect(screen.getByText(/stepOne\.website\.totalPageScraped/)).toBeInTheDocument()
})
it('should apply custom className', () => {
const { container } = render(<Crawling crawledNum={1} totalNum={5} className="custom" />)
expect(container.querySelector('.custom')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import ErrorMessage from '../error-message'
describe('ErrorMessage', () => {
it('should render title', () => {
render(<ErrorMessage title="Something went wrong" />)
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
})
it('should render error message when provided', () => {
render(<ErrorMessage title="Error" errorMsg="Detailed error info" />)
expect(screen.getByText('Detailed error info')).toBeInTheDocument()
})
it('should not render error message when not provided', () => {
const { container } = render(<ErrorMessage title="Error" />)
const textElements = container.querySelectorAll('.system-xs-regular')
expect(textElements).toHaveLength(0)
})
it('should apply custom className', () => {
const { container } = render(<ErrorMessage title="Error" className="custom-cls" />)
expect(container.querySelector('.custom-cls')).toBeInTheDocument()
})
})

View File

@ -1,15 +1,11 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import CheckboxWithLabel from './checkbox-with-label'
import CrawledResult from './crawled-result'
import CrawledResultItem from './crawled-result-item'
import Crawling from './crawling'
import ErrorMessage from './error-message'
// ==========================================
// Test Data Builders
// ==========================================
import CheckboxWithLabel from '../checkbox-with-label'
import CrawledResult from '../crawled-result'
import CrawledResultItem from '../crawled-result-item'
import Crawling from '../crawling'
import ErrorMessage from '../error-message'
const createMockCrawlResultItem = (overrides?: Partial<CrawlResultItemType>): CrawlResultItemType => ({
source_url: 'https://example.com/page1',
@ -27,9 +23,7 @@ const createMockCrawlResultItems = (count = 3): CrawlResultItemType[] => {
}))
}
// ==========================================
// CheckboxWithLabel Tests
// ==========================================
describe('CheckboxWithLabel', () => {
const defaultProps = {
isChecked: false,
@ -43,15 +37,12 @@ describe('CheckboxWithLabel', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<CheckboxWithLabel {...defaultProps} />)
// Assert
expect(screen.getByText('Test Label')).toBeInTheDocument()
})
it('should render checkbox in unchecked state', () => {
// Arrange & Act
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={false} />)
// Assert - Custom checkbox component uses div with data-testid
@ -61,7 +52,6 @@ describe('CheckboxWithLabel', () => {
})
it('should render checkbox in checked state', () => {
// Arrange & Act
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={true} />)
// Assert - Checked state has check icon
@ -70,7 +60,6 @@ describe('CheckboxWithLabel', () => {
})
it('should render tooltip when provided', () => {
// Arrange & Act
render(<CheckboxWithLabel {...defaultProps} tooltip="Helpful tooltip text" />)
// Assert - Tooltip trigger should be present
@ -79,10 +68,8 @@ describe('CheckboxWithLabel', () => {
})
it('should not render tooltip when not provided', () => {
// Arrange & Act
render(<CheckboxWithLabel {...defaultProps} />)
// Assert
const tooltipTrigger = document.querySelector('[class*="ml-0.5"]')
expect(tooltipTrigger).not.toBeInTheDocument()
})
@ -90,21 +77,17 @@ describe('CheckboxWithLabel', () => {
describe('Props', () => {
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<CheckboxWithLabel {...defaultProps} className="custom-class" />,
)
// Assert
const label = container.querySelector('label')
expect(label).toHaveClass('custom-class')
})
it('should apply custom labelClassName', () => {
// Arrange & Act
render(<CheckboxWithLabel {...defaultProps} labelClassName="custom-label-class" />)
// Assert
const labelText = screen.getByText('Test Label')
expect(labelText).toHaveClass('custom-label-class')
})
@ -112,33 +95,26 @@ describe('CheckboxWithLabel', () => {
describe('User Interactions', () => {
it('should call onChange with true when clicking unchecked checkbox', () => {
// Arrange
const mockOnChange = vi.fn()
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={false} onChange={mockOnChange} />)
// Act
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
// Assert
expect(mockOnChange).toHaveBeenCalledWith(true)
})
it('should call onChange with false when clicking checked checkbox', () => {
// Arrange
const mockOnChange = vi.fn()
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={true} onChange={mockOnChange} />)
// Act
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
// Assert
expect(mockOnChange).toHaveBeenCalledWith(false)
})
it('should not trigger onChange when clicking label text due to custom checkbox', () => {
// Arrange
const mockOnChange = vi.fn()
render(<CheckboxWithLabel {...defaultProps} onChange={mockOnChange} />)
@ -152,9 +128,7 @@ describe('CheckboxWithLabel', () => {
})
})
// ==========================================
// CrawledResultItem Tests
// ==========================================
describe('CrawledResultItem', () => {
const defaultProps = {
payload: createMockCrawlResultItem(),
@ -171,16 +145,13 @@ describe('CrawledResultItem', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<CrawledResultItem {...defaultProps} />)
// Assert
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
expect(screen.getByText('https://example.com/page1')).toBeInTheDocument()
})
it('should render checkbox when isMultipleChoice is true', () => {
// Arrange & Act
const { container } = render(<CrawledResultItem {...defaultProps} isMultipleChoice={true} />)
// Assert - Custom checkbox uses data-testid
@ -189,7 +160,6 @@ describe('CrawledResultItem', () => {
})
it('should render radio when isMultipleChoice is false', () => {
// Arrange & Act
const { container } = render(<CrawledResultItem {...defaultProps} isMultipleChoice={false} />)
// Assert - Radio component has size-4 rounded-full classes
@ -198,7 +168,6 @@ describe('CrawledResultItem', () => {
})
it('should render checkbox as checked when isChecked is true', () => {
// Arrange & Act
const { container } = render(<CrawledResultItem {...defaultProps} isChecked={true} />)
// Assert - Checked state shows check icon
@ -207,35 +176,27 @@ describe('CrawledResultItem', () => {
})
it('should render preview button when showPreview is true', () => {
// Arrange & Act
render(<CrawledResultItem {...defaultProps} showPreview={true} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should not render preview button when showPreview is false', () => {
// Arrange & Act
render(<CrawledResultItem {...defaultProps} showPreview={false} />)
// Assert
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})
it('should apply active background when isPreview is true', () => {
// Arrange & Act
const { container } = render(<CrawledResultItem {...defaultProps} isPreview={true} />)
// Assert
const item = container.firstChild
expect(item).toHaveClass('bg-state-base-active')
})
it('should apply hover styles when isPreview is false', () => {
// Arrange & Act
const { container } = render(<CrawledResultItem {...defaultProps} isPreview={false} />)
// Assert
const item = container.firstChild
expect(item).toHaveClass('group')
expect(item).toHaveClass('hover:bg-state-base-hover')
@ -244,35 +205,26 @@ describe('CrawledResultItem', () => {
describe('Props', () => {
it('should display payload title', () => {
// Arrange
const payload = createMockCrawlResultItem({ title: 'Custom Title' })
// Act
render(<CrawledResultItem {...defaultProps} payload={payload} />)
// Assert
expect(screen.getByText('Custom Title')).toBeInTheDocument()
})
it('should display payload source_url', () => {
// Arrange
const payload = createMockCrawlResultItem({ source_url: 'https://custom.url/path' })
// Act
render(<CrawledResultItem {...defaultProps} payload={payload} />)
// Assert
expect(screen.getByText('https://custom.url/path')).toBeInTheDocument()
})
it('should set title attribute for truncation tooltip', () => {
// Arrange
const payload = createMockCrawlResultItem({ title: 'Very Long Title' })
// Act
render(<CrawledResultItem {...defaultProps} payload={payload} />)
// Assert
const titleElement = screen.getByText('Very Long Title')
expect(titleElement).toHaveAttribute('title', 'Very Long Title')
})
@ -280,7 +232,6 @@ describe('CrawledResultItem', () => {
describe('User Interactions', () => {
it('should call onCheckChange with true when clicking unchecked checkbox', () => {
// Arrange
const mockOnCheckChange = vi.fn()
const { container } = render(
<CrawledResultItem
@ -290,16 +241,13 @@ describe('CrawledResultItem', () => {
/>,
)
// Act
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
// Assert
expect(mockOnCheckChange).toHaveBeenCalledWith(true)
})
it('should call onCheckChange with false when clicking checked checkbox', () => {
// Arrange
const mockOnCheckChange = vi.fn()
const { container } = render(
<CrawledResultItem
@ -309,28 +257,22 @@ describe('CrawledResultItem', () => {
/>,
)
// Act
const checkbox = container.querySelector('[data-testid^="checkbox"]')!
fireEvent.click(checkbox)
// Assert
expect(mockOnCheckChange).toHaveBeenCalledWith(false)
})
it('should call onPreview when clicking preview button', () => {
// Arrange
const mockOnPreview = vi.fn()
render(<CrawledResultItem {...defaultProps} onPreview={mockOnPreview} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnPreview).toHaveBeenCalled()
})
it('should toggle radio state when isMultipleChoice is false', () => {
// Arrange
const mockOnCheckChange = vi.fn()
const { container } = render(
<CrawledResultItem
@ -345,15 +287,12 @@ describe('CrawledResultItem', () => {
const radio = container.querySelector('.size-4.rounded-full')!
fireEvent.click(radio)
// Assert
expect(mockOnCheckChange).toHaveBeenCalledWith(true)
})
})
})
// ==========================================
// CrawledResult Tests
// ==========================================
describe('CrawledResult', () => {
const defaultProps = {
list: createMockCrawlResultItems(3),
@ -368,7 +307,6 @@ describe('CrawledResult', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} />)
// Assert - Check for time info which contains total count
@ -376,17 +314,14 @@ describe('CrawledResult', () => {
})
it('should render all list items', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} />)
// Assert
expect(screen.getByText('Page 1')).toBeInTheDocument()
expect(screen.getByText('Page 2')).toBeInTheDocument()
expect(screen.getByText('Page 3')).toBeInTheDocument()
})
it('should display scrape time info', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} usedTime={2.5} />)
// Assert - Check for the time display
@ -394,7 +329,6 @@ describe('CrawledResult', () => {
})
it('should render select all checkbox when isMultipleChoice is true', () => {
// Arrange & Act
const { container } = render(<CrawledResult {...defaultProps} isMultipleChoice={true} />)
// Assert - Multiple custom checkboxes (select all + items)
@ -403,7 +337,6 @@ describe('CrawledResult', () => {
})
it('should not render select all checkbox when isMultipleChoice is false', () => {
// Arrange & Act
const { container } = render(<CrawledResult {...defaultProps} isMultipleChoice={false} />)
// Assert - No select all checkbox, only radio buttons for items
@ -415,38 +348,30 @@ describe('CrawledResult', () => {
})
it('should show "Select All" when not all items are checked', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} checkedList={[]} />)
// Assert
expect(screen.getByText(/selectAll|Select All/i)).toBeInTheDocument()
})
it('should show "Reset All" when all items are checked', () => {
// Arrange
const allChecked = createMockCrawlResultItems(3)
// Act
render(<CrawledResult {...defaultProps} checkedList={allChecked} />)
// Assert
expect(screen.getByText(/resetAll|Reset All/i)).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<CrawledResult {...defaultProps} className="custom-class" />,
)
// Assert
expect(container.firstChild).toHaveClass('custom-class')
})
it('should highlight item at previewIndex', () => {
// Arrange & Act
const { container } = render(
<CrawledResult {...defaultProps} previewIndex={1} />,
)
@ -457,7 +382,6 @@ describe('CrawledResult', () => {
})
it('should pass showPreview to items', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} showPreview={true} />)
// Assert - Preview buttons should be visible
@ -466,17 +390,14 @@ describe('CrawledResult', () => {
})
it('should not show preview buttons when showPreview is false', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} showPreview={false} />)
// Assert
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onSelectedChange with all items when clicking select all', () => {
// Arrange
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
@ -492,12 +413,10 @@ describe('CrawledResult', () => {
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[0])
// Assert
expect(mockOnSelectedChange).toHaveBeenCalledWith(list)
})
it('should call onSelectedChange with empty array when clicking reset all', () => {
// Arrange
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
@ -509,16 +428,13 @@ describe('CrawledResult', () => {
/>,
)
// Act
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[0])
// Assert
expect(mockOnSelectedChange).toHaveBeenCalledWith([])
})
it('should add item to checkedList when checking unchecked item', () => {
// Arrange
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
@ -534,12 +450,10 @@ describe('CrawledResult', () => {
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[2])
// Assert
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[0], list[1]])
})
it('should remove item from checkedList when unchecking checked item', () => {
// Arrange
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
@ -555,12 +469,10 @@ describe('CrawledResult', () => {
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[1])
// Assert
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[1]])
})
it('should replace selection when checking in single choice mode', () => {
// Arrange
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
@ -582,7 +494,6 @@ describe('CrawledResult', () => {
})
it('should call onPreview with item and index when clicking preview', () => {
// Arrange
const mockOnPreview = vi.fn()
const list = createMockCrawlResultItems(3)
render(
@ -594,11 +505,9 @@ describe('CrawledResult', () => {
/>,
)
// Act
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[1]) // Second item's preview button
// Assert
expect(mockOnPreview).toHaveBeenCalledWith(list[1], 1)
})
@ -625,7 +534,6 @@ describe('CrawledResult', () => {
describe('Edge Cases', () => {
it('should handle empty list', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} list={[]} usedTime={0.5} />)
// Assert - Should show time info with 0 count
@ -633,29 +541,22 @@ describe('CrawledResult', () => {
})
it('should handle single item list', () => {
// Arrange
const singleItem = [createMockCrawlResultItem()]
// Act
render(<CrawledResult {...defaultProps} list={singleItem} />)
// Assert
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should format usedTime to one decimal place', () => {
// Arrange & Act
render(<CrawledResult {...defaultProps} usedTime={1.567} />)
// Assert
expect(screen.getByText(/1.6/)).toBeInTheDocument()
})
})
})
// ==========================================
// Crawling Tests
// ==========================================
describe('Crawling', () => {
const defaultProps = {
crawledNum: 5,
@ -668,23 +569,18 @@ describe('Crawling', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<Crawling {...defaultProps} />)
// Assert
expect(screen.getByText(/5\/10/)).toBeInTheDocument()
})
it('should display crawled count and total', () => {
// Arrange & Act
render(<Crawling crawledNum={3} totalNum={15} />)
// Assert
expect(screen.getByText(/3\/15/)).toBeInTheDocument()
})
it('should render skeleton items', () => {
// Arrange & Act
const { container } = render(<Crawling {...defaultProps} />)
// Assert - Should have 3 skeleton items
@ -693,10 +589,8 @@ describe('Crawling', () => {
})
it('should render header skeleton block', () => {
// Arrange & Act
const { container } = render(<Crawling {...defaultProps} />)
// Assert
const headerBlocks = container.querySelectorAll('.px-4.py-2 .bg-text-quaternary')
expect(headerBlocks.length).toBeGreaterThan(0)
})
@ -704,35 +598,28 @@ describe('Crawling', () => {
describe('Props', () => {
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<Crawling {...defaultProps} className="custom-crawling-class" />,
)
// Assert
expect(container.firstChild).toHaveClass('custom-crawling-class')
})
it('should handle zero values', () => {
// Arrange & Act
render(<Crawling crawledNum={0} totalNum={0} />)
// Assert
expect(screen.getByText(/0\/0/)).toBeInTheDocument()
})
it('should handle large numbers', () => {
// Arrange & Act
render(<Crawling crawledNum={999} totalNum={1000} />)
// Assert
expect(screen.getByText(/999\/1000/)).toBeInTheDocument()
})
})
describe('Skeleton Structure', () => {
it('should render blocks with correct width classes', () => {
// Arrange & Act
const { container } = render(<Crawling {...defaultProps} />)
// Assert - Check for various width classes
@ -743,9 +630,7 @@ describe('Crawling', () => {
})
})
// ==========================================
// ErrorMessage Tests
// ==========================================
describe('ErrorMessage', () => {
const defaultProps = {
title: 'Error Title',
@ -757,41 +642,32 @@ describe('ErrorMessage', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<ErrorMessage {...defaultProps} />)
// Assert
expect(screen.getByText('Error Title')).toBeInTheDocument()
})
it('should render error icon', () => {
// Arrange & Act
const { container } = render(<ErrorMessage {...defaultProps} />)
// Assert
const icon = container.querySelector('svg')
expect(icon).toBeInTheDocument()
expect(icon).toHaveClass('text-text-destructive')
})
it('should render title', () => {
// Arrange & Act
render(<ErrorMessage title="Custom Error Title" />)
// Assert
expect(screen.getByText('Custom Error Title')).toBeInTheDocument()
})
it('should render error message when provided', () => {
// Arrange & Act
render(<ErrorMessage {...defaultProps} errorMsg="Detailed error description" />)
// Assert
expect(screen.getByText('Detailed error description')).toBeInTheDocument()
})
it('should not render error message when not provided', () => {
// Arrange & Act
render(<ErrorMessage {...defaultProps} />)
// Assert - Should only have title, not error message container
@ -802,17 +678,14 @@ describe('ErrorMessage', () => {
describe('Props', () => {
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<ErrorMessage {...defaultProps} className="custom-error-class" />,
)
// Assert
expect(container.firstChild).toHaveClass('custom-error-class')
})
it('should render with empty errorMsg', () => {
// Arrange & Act
render(<ErrorMessage {...defaultProps} errorMsg="" />)
// Assert - Empty string should not render message div
@ -820,64 +693,47 @@ describe('ErrorMessage', () => {
})
it('should handle long title text', () => {
// Arrange
const longTitle = 'This is a very long error title that might wrap to multiple lines'
// Act
render(<ErrorMessage title={longTitle} />)
// Assert
expect(screen.getByText(longTitle)).toBeInTheDocument()
})
it('should handle long error message', () => {
// Arrange
const longErrorMsg = 'This is a very detailed error message explaining what went wrong and how to fix it. It contains multiple sentences.'
// Act
render(<ErrorMessage {...defaultProps} errorMsg={longErrorMsg} />)
// Assert
expect(screen.getByText(longErrorMsg)).toBeInTheDocument()
})
})
describe('Styling', () => {
it('should have error background styling', () => {
// Arrange & Act
const { container } = render(<ErrorMessage {...defaultProps} />)
// Assert
expect(container.firstChild).toHaveClass('bg-toast-error-bg')
})
it('should have border styling', () => {
// Arrange & Act
const { container } = render(<ErrorMessage {...defaultProps} />)
// Assert
expect(container.firstChild).toHaveClass('border-components-panel-border')
})
it('should have rounded corners', () => {
// Arrange & Act
const { container } = render(<ErrorMessage {...defaultProps} />)
// Assert
expect(container.firstChild).toHaveClass('rounded-xl')
})
})
})
// ==========================================
// Integration Tests
// ==========================================
describe('Base Components Integration', () => {
it('should render CrawledResult with CrawledResultItem children', () => {
// Arrange
const list = createMockCrawlResultItems(2)
// Act
render(
<CrawledResult
list={list}
@ -893,10 +749,8 @@ describe('Base Components Integration', () => {
})
it('should render CrawledResult with CheckboxWithLabel for select all', () => {
// Arrange
const list = createMockCrawlResultItems(2)
// Act
const { container } = render(
<CrawledResult
list={list}
@ -913,7 +767,6 @@ describe('Base Components Integration', () => {
})
it('should allow selecting and previewing items', () => {
// Arrange
const list = createMockCrawlResultItems(3)
const mockOnSelectedChange = vi.fn()
const mockOnPreview = vi.fn()
@ -933,14 +786,12 @@ describe('Base Components Integration', () => {
const checkboxes = container.querySelectorAll('[data-testid^="checkbox"]')
fireEvent.click(checkboxes[1])
// Assert
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[0]])
// Act - Preview second item
const previewButtons = screen.getAllByRole('button')
fireEvent.click(previewButtons[1])
// Assert
expect(mockOnPreview).toHaveBeenCalledWith(list[1], 1)
})
})

View File

@ -6,13 +6,7 @@ import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/ty
import Toast from '@/app/components/base/toast'
import { CrawlStep } from '@/models/datasets'
import { PipelineInputVarType } from '@/models/pipeline'
import Options from './index'
// ==========================================
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/vitest.setup.ts
import Options from '../index'
// Mock useInitialData and useConfigurations hooks
const { mockUseInitialData, mockUseConfigurations } = vi.hoisted(() => ({
@ -28,15 +22,16 @@ vi.mock('@/app/components/rag-pipeline/hooks/use-input-fields', () => ({
// Mock BaseField
const mockBaseField = vi.fn()
vi.mock('@/app/components/base/form/form-scenarios/base/field', () => {
const MockBaseFieldFactory = (props: any) => {
const MockBaseFieldFactory = (props: Record<string, unknown>) => {
mockBaseField(props)
const MockField = ({ form }: { form: any }) => (
<div data-testid={`field-${props.config?.variable || 'unknown'}`}>
<span data-testid={`field-label-${props.config?.variable}`}>{props.config?.label}</span>
const config = props.config as { variable?: string, label?: string } | undefined
const MockField = ({ form }: { form: { getFieldValue?: (field: string) => string, setFieldValue?: (field: string, value: string) => void } }) => (
<div data-testid={`field-${config?.variable || 'unknown'}`}>
<span data-testid={`field-label-${config?.variable}`}>{config?.label}</span>
<input
data-testid={`field-input-${props.config?.variable}`}
value={form.getFieldValue?.(props.config?.variable) || ''}
onChange={e => form.setFieldValue?.(props.config?.variable, e.target.value)}
data-testid={`field-input-${config?.variable}`}
value={form.getFieldValue?.(config?.variable || '') || ''}
onChange={e => form.setFieldValue?.(config?.variable || '', e.target.value)}
/>
</div>
)
@ -47,9 +42,9 @@ vi.mock('@/app/components/base/form/form-scenarios/base/field', () => {
// Mock useAppForm
const mockHandleSubmit = vi.fn()
const mockFormValues: Record<string, any> = {}
const mockFormValues: Record<string, unknown> = {}
vi.mock('@/app/components/base/form', () => ({
useAppForm: (options: any) => {
useAppForm: (options: { validators?: { onSubmit?: (arg: { value: Record<string, unknown> }) => unknown }, onSubmit?: (arg: { value: Record<string, unknown> }) => void }) => {
const formOptions = options
return {
handleSubmit: () => {
@ -60,17 +55,13 @@ vi.mock('@/app/components/base/form', () => ({
}
},
getFieldValue: (field: string) => mockFormValues[field],
setFieldValue: (field: string, value: any) => {
setFieldValue: (field: string, value: unknown) => {
mockFormValues[field] = value
},
}
},
}))
// ==========================================
// Test Data Builders
// ==========================================
const createMockVariable = (overrides?: Partial<RAGPipelineVariables[0]>): RAGPipelineVariables[0] => ({
belong_to_node_id: 'node-1',
type: PipelineInputVarType.textInput,
@ -91,7 +82,18 @@ const createMockVariables = (count = 1): RAGPipelineVariables => {
}))
}
const createMockConfiguration = (overrides?: Partial<any>): any => ({
type MockConfiguration = {
type: BaseFieldType
variable: string
label: string
required: boolean
maxLength: number
options: unknown[]
showConditions: unknown[]
placeholder: string
}
const createMockConfiguration = (overrides?: Partial<MockConfiguration>): MockConfiguration => ({
type: BaseFieldType.textInput,
variable: 'test_variable',
label: 'Test Label',
@ -113,9 +115,6 @@ const createDefaultProps = (overrides?: Partial<OptionsProps>): OptionsProps =>
...overrides,
})
// ==========================================
// Test Suites
// ==========================================
describe('Options', () => {
let toastNotifySpy: MockInstance
@ -137,46 +136,33 @@ describe('Options', () => {
toastNotifySpy.mockRestore()
})
// ==========================================
// Rendering Tests
// ==========================================
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert
expect(container.querySelector('form')).toBeInTheDocument()
})
it('should render options header with toggle text', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByText(/options/i)).toBeInTheDocument()
})
it('should render Run button', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getByText(/run/i)).toBeInTheDocument()
})
it('should render form fields when not folded', () => {
// Arrange
const configurations = [
createMockConfiguration({ variable: 'url', label: 'URL' }),
createMockConfiguration({ variable: 'depth', label: 'Depth' }),
@ -184,19 +170,15 @@ describe('Options', () => {
mockUseConfigurations.mockReturnValue(configurations)
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByTestId('field-url')).toBeInTheDocument()
expect(screen.getByTestId('field-depth')).toBeInTheDocument()
})
it('should render arrow icon in correct orientation when expanded', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert - Arrow should not have -rotate-90 class when expanded
@ -206,37 +188,27 @@ describe('Options', () => {
})
})
// ==========================================
// Props Testing
// ==========================================
describe('Props', () => {
describe('variables prop', () => {
it('should pass variables to useInitialData hook', () => {
// Arrange
const variables = createMockVariables(3)
const props = createDefaultProps({ variables })
// Act
render(<Options {...props} />)
// Assert
expect(mockUseInitialData).toHaveBeenCalledWith(variables)
})
it('should pass variables to useConfigurations hook', () => {
// Arrange
const variables = createMockVariables(2)
const props = createDefaultProps({ variables })
// Act
render(<Options {...props} />)
// Assert
expect(mockUseConfigurations).toHaveBeenCalledWith(variables)
})
it('should render correct number of fields based on configurations', () => {
// Arrange
const configurations = [
createMockConfiguration({ variable: 'field_1', label: 'Field 1' }),
createMockConfiguration({ variable: 'field_2', label: 'Field 2' }),
@ -245,24 +217,19 @@ describe('Options', () => {
mockUseConfigurations.mockReturnValue(configurations)
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByTestId('field-field_1')).toBeInTheDocument()
expect(screen.getByTestId('field-field_2')).toBeInTheDocument()
expect(screen.getByTestId('field-field_3')).toBeInTheDocument()
})
it('should handle empty variables array', () => {
// Arrange
mockUseConfigurations.mockReturnValue([])
const props = createDefaultProps({ variables: [] })
// Act
const { container } = render(<Options {...props} />)
// Assert
expect(container.querySelector('form')).toBeInTheDocument()
expect(screen.queryByTestId(/field-/)).not.toBeInTheDocument()
})
@ -270,54 +237,40 @@ describe('Options', () => {
describe('step prop', () => {
it('should show "Run" text when step is init', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.init })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByText(/run/i)).toBeInTheDocument()
})
it('should show "Running" text when step is running', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.running })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByText(/running/i)).toBeInTheDocument()
})
it('should disable button when step is running', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.running })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
it('should enable button when step is finished', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.finished, runDisabled: false })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
it('should show loading state on button when step is running', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.running })
// Act
render(<Options {...props} />)
// Assert - Button should have loading prop which disables it
@ -328,47 +281,35 @@ describe('Options', () => {
describe('runDisabled prop', () => {
it('should disable button when runDisabled is true', () => {
// Arrange
const props = createDefaultProps({ runDisabled: true })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
it('should enable button when runDisabled is false and step is not running', () => {
// Arrange
const props = createDefaultProps({ runDisabled: false, step: CrawlStep.init })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
it('should disable button when both runDisabled is true and step is running', () => {
// Arrange
const props = createDefaultProps({ runDisabled: true, step: CrawlStep.running })
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
it('should default runDisabled to undefined (falsy)', () => {
// Arrange
const props = createDefaultProps()
delete (props as any).runDisabled
delete (props as Partial<OptionsProps>).runDisabled
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
})
@ -385,16 +326,13 @@ describe('Options', () => {
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
// Act
render(<Options {...props} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).toHaveBeenCalled()
})
it('should not call onSubmit when validation fails', () => {
// Arrange
const mockOnSubmit = vi.fn()
// Create a required field configuration
const requiredConfig = createMockConfiguration({
@ -407,11 +345,9 @@ describe('Options', () => {
// mockFormValues is empty, so required field validation will fail
const props = createDefaultProps({ onSubmit: mockOnSubmit })
// Act
render(<Options {...props} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).not.toHaveBeenCalled()
})
@ -427,22 +363,17 @@ describe('Options', () => {
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
// Act
render(<Options {...props} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).toHaveBeenCalledWith({ url: 'https://example.com', depth: 2 })
})
})
})
// ==========================================
// Side Effects and Cleanup (useEffect)
// ==========================================
describe('Side Effects and Cleanup', () => {
it('should expand options when step changes to init', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.finished })
const { rerender, container } = render(<Options {...props} />)
@ -456,7 +387,6 @@ describe('Options', () => {
})
it('should collapse options when step changes to running', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.init })
const { rerender, container } = render(<Options {...props} />)
@ -473,7 +403,6 @@ describe('Options', () => {
})
it('should collapse options when step changes to finished', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.init })
const { rerender, container } = render(<Options {...props} />)
@ -487,7 +416,6 @@ describe('Options', () => {
})
it('should respond to step transitions from init -> running -> finished', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.init })
const { rerender, container } = render(<Options {...props} />)
@ -512,7 +440,6 @@ describe('Options', () => {
})
it('should expand when step transitions from finished to init', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.finished })
const { rerender } = render(<Options {...props} />)
@ -527,12 +454,9 @@ describe('Options', () => {
})
})
// ==========================================
// Memoization Logic and Dependencies
// ==========================================
describe('Memoization Logic and Dependencies', () => {
it('should regenerate schema when configurations change', () => {
// Arrange
const config1 = [createMockConfiguration({ variable: 'url' })]
const config2 = [createMockConfiguration({ variable: 'depth' })]
mockUseConfigurations.mockReturnValue(config1)
@ -551,10 +475,8 @@ describe('Options', () => {
})
it('should compute isRunning correctly for init step', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.init })
// Act
render(<Options {...props} />)
// Assert - Button should not be in loading state
@ -564,10 +486,8 @@ describe('Options', () => {
})
it('should compute isRunning correctly for running step', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.running })
// Act
render(<Options {...props} />)
// Assert - Button should be in loading state
@ -577,10 +497,8 @@ describe('Options', () => {
})
it('should compute isRunning correctly for finished step', () => {
// Arrange
const props = createDefaultProps({ step: CrawlStep.finished })
// Act
render(<Options {...props} />)
// Assert - Button should not be in loading state
@ -606,12 +524,9 @@ describe('Options', () => {
})
})
// ==========================================
// User Interactions and Event Handlers
// ==========================================
describe('User Interactions and Event Handlers', () => {
it('should toggle fold state when header is clicked', () => {
// Arrange
const props = createDefaultProps()
render(<Options {...props} />)
@ -632,11 +547,9 @@ describe('Options', () => {
})
it('should prevent default and stop propagation on form submit', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<Options {...props} />)
// Act
const form = container.querySelector('form')!
const mockPreventDefault = vi.fn()
const mockStopPropagation = vi.fn()
@ -662,15 +575,12 @@ describe('Options', () => {
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).toHaveBeenCalled()
})
it('should not trigger submit when button is disabled', () => {
// Arrange
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit, runDisabled: true })
render(<Options {...props} />)
@ -678,12 +588,10 @@ describe('Options', () => {
// Act - Try to click disabled button
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).not.toHaveBeenCalled()
})
it('should maintain fold state after form submission', () => {
// Arrange
const props = createDefaultProps()
render(<Options {...props} />)
@ -698,7 +606,6 @@ describe('Options', () => {
})
it('should allow clicking on arrow icon container to toggle', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<Options {...props} />)
@ -714,9 +621,6 @@ describe('Options', () => {
})
})
// ==========================================
// Edge Cases and Error Handling
// ==========================================
describe('Edge Cases and Error Handling', () => {
it('should handle validation error and show toast', () => {
// Arrange - Create required field that will fail validation when empty
@ -731,7 +635,6 @@ describe('Options', () => {
const props = createDefaultProps()
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert - Toast should be called with error message
@ -754,7 +657,6 @@ describe('Options', () => {
const props = createDefaultProps()
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert - Toast message should contain field path
@ -767,11 +669,9 @@ describe('Options', () => {
})
it('should handle empty variables gracefully', () => {
// Arrange
mockUseConfigurations.mockReturnValue([])
const props = createDefaultProps({ variables: [] })
// Act
const { container } = render(<Options {...props} />)
// Assert - Should render without errors
@ -780,29 +680,23 @@ describe('Options', () => {
})
it('should handle single variable configuration', () => {
// Arrange
const singleConfig = [createMockConfiguration({ variable: 'only_field' })]
mockUseConfigurations.mockReturnValue(singleConfig)
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
expect(screen.getByTestId('field-only_field')).toBeInTheDocument()
})
it('should handle many configurations', () => {
// Arrange
const manyConfigs = Array.from({ length: 10 }, (_, i) =>
createMockConfiguration({ variable: `field_${i}`, label: `Field ${i}` }))
mockUseConfigurations.mockReturnValue(manyConfigs)
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
for (let i = 0; i < 10; i++)
expect(screen.getByTestId(`field-field_${i}`)).toBeInTheDocument()
})
@ -817,7 +711,6 @@ describe('Options', () => {
const props = createDefaultProps()
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert - Toast should be called once (only first error)
@ -830,7 +723,6 @@ describe('Options', () => {
})
it('should handle validation pass when all required fields have values', () => {
// Arrange
const requiredConfig = createMockConfiguration({
variable: 'url',
label: 'URL',
@ -843,7 +735,6 @@ describe('Options', () => {
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert - No toast error, onSubmit called
@ -852,17 +743,15 @@ describe('Options', () => {
})
it('should handle undefined variables gracefully', () => {
// Arrange
mockUseInitialData.mockReturnValue({})
mockUseConfigurations.mockReturnValue([])
const props = createDefaultProps({ variables: undefined as any })
const props = createDefaultProps({ variables: undefined as unknown as RAGPipelineVariables })
// Act & Assert - Should not throw
expect(() => render(<Options {...props} />)).not.toThrow()
})
it('should handle rapid fold/unfold toggling', () => {
// Arrange
const props = createDefaultProps()
render(<Options {...props} />)
@ -876,9 +765,7 @@ describe('Options', () => {
})
})
// ==========================================
// All Prop Variations
// ==========================================
describe('Prop Variations', () => {
it.each([
[{ step: CrawlStep.init, runDisabled: false }, false, 'run'],
@ -888,13 +775,10 @@ describe('Options', () => {
[{ step: CrawlStep.finished, runDisabled: false }, false, 'run'],
[{ step: CrawlStep.finished, runDisabled: true }, true, 'run'],
] as const)('should render correctly with step=%s, runDisabled=%s', (propVariation, expectedDisabled, expectedText) => {
// Arrange
const props = createDefaultProps(propVariation)
// Act
render(<Options {...props} />)
// Assert
const button = screen.getByRole('button')
if (expectedDisabled)
expect(button).toBeDisabled()
@ -915,7 +799,6 @@ describe('Options', () => {
})
it('should handle variables with different types', () => {
// Arrange
const variables: RAGPipelineVariables = [
createMockVariable({ type: PipelineInputVarType.textInput, variable: 'text_field' }),
createMockVariable({ type: PipelineInputVarType.paragraph, variable: 'paragraph_field' }),
@ -927,19 +810,15 @@ describe('Options', () => {
mockUseConfigurations.mockReturnValue(configurations)
const props = createDefaultProps({ variables })
// Act
render(<Options {...props} />)
// Assert
variables.forEach((v) => {
expect(screen.getByTestId(`field-${v.variable}`)).toBeInTheDocument()
})
})
})
// ==========================================
// Form Validation
// ==========================================
describe('Form Validation', () => {
it('should pass validation with valid data', () => {
// Arrange - Use non-required field so empty value passes
@ -953,10 +832,8 @@ describe('Options', () => {
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).toHaveBeenCalled()
expect(toastNotifySpy).not.toHaveBeenCalled()
})
@ -974,10 +851,8 @@ describe('Options', () => {
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnSubmit).not.toHaveBeenCalled()
expect(toastNotifySpy).toHaveBeenCalled()
})
@ -994,10 +869,8 @@ describe('Options', () => {
const props = createDefaultProps()
render(<Options {...props} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(toastNotifySpy).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
@ -1007,99 +880,75 @@ describe('Options', () => {
})
})
// ==========================================
// Styling Tests
// ==========================================
describe('Styling', () => {
it('should apply correct container classes to form', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert
const form = container.querySelector('form')
expect(form).toHaveClass('w-full')
})
it('should apply cursor-pointer class to toggle container', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert
const toggleContainer = container.querySelector('.cursor-pointer')
expect(toggleContainer).toBeInTheDocument()
})
it('should apply select-none class to prevent text selection on toggle', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert
const toggleContainer = container.querySelector('.select-none')
expect(toggleContainer).toBeInTheDocument()
})
it('should apply rotate class to arrow icon when folded', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<Options {...props} />)
// Act - Fold the options
fireEvent.click(screen.getByText(/options/i))
// Assert
const arrowIcon = container.querySelector('svg')
expect(arrowIcon).toHaveClass('-rotate-90')
})
it('should not apply rotate class to arrow icon when expanded', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert
const arrowIcon = container.querySelector('svg')
expect(arrowIcon).not.toHaveClass('-rotate-90')
})
it('should apply border class to fields container when expanded', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<Options {...props} />)
// Assert
const fieldsContainer = container.querySelector('.border-t')
expect(fieldsContainer).toBeInTheDocument()
})
})
// ==========================================
// BaseField Integration
// ==========================================
describe('BaseField Integration', () => {
it('should pass correct props to BaseField factory', () => {
// Arrange
const config = createMockConfiguration({ variable: 'test_var', label: 'Test Label' })
mockUseConfigurations.mockReturnValue([config])
mockUseInitialData.mockReturnValue({ test_var: 'default_value' })
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert
expect(mockBaseField).toHaveBeenCalledWith(
expect.objectContaining({
initialData: { test_var: 'default_value' },
@ -1109,7 +958,6 @@ describe('Options', () => {
})
it('should render unique key for each field', () => {
// Arrange
const configurations = [
createMockConfiguration({ variable: 'field_a' }),
createMockConfiguration({ variable: 'field_b' }),
@ -1118,7 +966,6 @@ describe('Options', () => {
mockUseConfigurations.mockReturnValue(configurations)
const props = createDefaultProps()
// Act
render(<Options {...props} />)
// Assert - All fields should be rendered (React would warn if keys aren't unique)