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

@ -1,14 +1,10 @@
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
// ============================================================================
import CrawledResult from '../base/crawled-result'
import CrawledResultItem from '../base/crawled-result-item'
import Header from '../base/header'
import Input from '../base/input'
const createCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page Title',
@ -18,9 +14,7 @@ const createCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): CrawlR
...overrides,
})
// ============================================================================
// Input Component Tests
// ============================================================================
describe('Input', () => {
beforeEach(() => {
@ -155,9 +149,7 @@ describe('Input', () => {
})
})
// ============================================================================
// Header Component Tests
// ============================================================================
describe('Header', () => {
const createHeaderProps = (overrides: Partial<Parameters<typeof Header>[0]> = {}) => ({
@ -254,9 +246,7 @@ describe('Header', () => {
})
})
// ============================================================================
// CrawledResultItem Component Tests
// ============================================================================
describe('CrawledResultItem', () => {
const createItemProps = (overrides: Partial<Parameters<typeof CrawledResultItem>[0]> = {}) => ({
@ -359,9 +349,7 @@ describe('CrawledResultItem', () => {
})
})
// ============================================================================
// CrawledResult Component Tests
// ============================================================================
describe('CrawledResult', () => {
const createResultProps = (overrides: Partial<Parameters<typeof CrawledResult>[0]> = {}) => ({
@ -487,7 +475,6 @@ describe('CrawledResult', () => {
const props = createResultProps({ list, checkedList: [list[0], list[1]], onSelectedChange })
render(<CrawledResult {...props} />)
// Click the first item's checkbox to uncheck it
await userEvent.click(getItemCheckbox(0))
expect(onSelectedChange).toHaveBeenCalledWith([list[1]])
@ -505,7 +492,6 @@ describe('CrawledResult', () => {
render(<CrawledResult {...props} />)
// Click preview on second item
const previewButtons = screen.getAllByText('datasetCreation.stepOne.website.preview')
await userEvent.click(previewButtons[1])
@ -522,7 +508,6 @@ describe('CrawledResult', () => {
render(<CrawledResult {...props} />)
// Click preview on first item
const previewButtons = screen.getAllByText('datasetCreation.stepOne.website.preview')
await userEvent.click(previewButtons[0])

View File

@ -0,0 +1,286 @@
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
import Website from '../index'
const mockSetShowAccountSettingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowAccountSettingModal: mockSetShowAccountSettingModal,
}),
}))
vi.mock('../index.module.css', () => ({
default: {
jinaLogo: 'jina-logo',
watercrawlLogo: 'watercrawl-logo',
},
}))
vi.mock('../firecrawl', () => ({
default: (props: Record<string, unknown>) => <div data-testid="firecrawl-component" data-props={JSON.stringify(props)} />,
}))
vi.mock('../jina-reader', () => ({
default: (props: Record<string, unknown>) => <div data-testid="jina-reader-component" data-props={JSON.stringify(props)} />,
}))
vi.mock('../watercrawl', () => ({
default: (props: Record<string, unknown>) => <div data-testid="watercrawl-component" data-props={JSON.stringify(props)} />,
}))
vi.mock('../no-data', () => ({
default: ({ onConfig, provider }: { onConfig: () => void, provider: string }) => (
<div data-testid="no-data-component" data-provider={provider}>
<button onClick={onConfig} data-testid="no-data-config-button">Configure</button>
</div>
),
}))
let mockEnableJinaReader = true
let mockEnableFirecrawl = true
let mockEnableWatercrawl = true
vi.mock('@/config', () => ({
get ENABLE_WEBSITE_JINAREADER() { return mockEnableJinaReader },
get ENABLE_WEBSITE_FIRECRAWL() { return mockEnableFirecrawl },
get ENABLE_WEBSITE_WATERCRAWL() { return mockEnableWatercrawl },
}))
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,
})
const createMockDataSourceAuth = (
provider: string,
credentialsCount = 1,
): DataSourceAuth => ({
author: 'test',
provider,
plugin_id: `${provider}-plugin`,
plugin_unique_identifier: `${provider}-unique`,
icon: 'icon.png',
name: provider,
label: { en_US: provider, zh_Hans: provider },
description: { en_US: `${provider} description`, zh_Hans: `${provider} description` },
credentials_list: Array.from({ length: credentialsCount }, (_, i) => ({
credential: {},
type: CredentialTypeEnum.API_KEY,
name: `cred-${i}`,
id: `cred-${i}`,
is_default: i === 0,
avatar_url: '',
})),
})
type RenderProps = {
authedDataSourceList?: DataSourceAuth[]
enableJina?: boolean
enableFirecrawl?: boolean
enableWatercrawl?: boolean
}
const renderWebsite = ({
authedDataSourceList = [],
enableJina = true,
enableFirecrawl = true,
enableWatercrawl = true,
}: RenderProps = {}) => {
mockEnableJinaReader = enableJina
mockEnableFirecrawl = enableFirecrawl
mockEnableWatercrawl = enableWatercrawl
const props = {
onPreview: vi.fn() as (payload: CrawlResultItem) => void,
checkedCrawlResult: [] as CrawlResultItem[],
onCheckedCrawlResultChange: vi.fn() as (payload: CrawlResultItem[]) => void,
onCrawlProviderChange: vi.fn(),
onJobIdChange: vi.fn(),
crawlOptions: createMockCrawlOptions(),
onCrawlOptionsChange: vi.fn() as (payload: CrawlOptions) => void,
authedDataSourceList,
}
const result = render(<Website {...props} />)
return { ...result, props }
}
describe('Website', () => {
beforeEach(() => {
vi.clearAllMocks()
mockEnableJinaReader = true
mockEnableFirecrawl = true
mockEnableWatercrawl = true
})
describe('Rendering', () => {
it('should render provider selection section', () => {
renderWebsite()
expect(screen.getByText(/chooseProvider/i)).toBeInTheDocument()
})
it('should show Jina Reader button when ENABLE_WEBSITE_JINAREADER is true', () => {
renderWebsite({ enableJina: true })
expect(screen.getByText('Jina Reader')).toBeInTheDocument()
})
it('should not show Jina Reader button when ENABLE_WEBSITE_JINAREADER is false', () => {
renderWebsite({ enableJina: false })
expect(screen.queryByText('Jina Reader')).not.toBeInTheDocument()
})
it('should show Firecrawl button when ENABLE_WEBSITE_FIRECRAWL is true', () => {
renderWebsite({ enableFirecrawl: true })
expect(screen.getByText(/Firecrawl/)).toBeInTheDocument()
})
it('should not show Firecrawl button when ENABLE_WEBSITE_FIRECRAWL is false', () => {
renderWebsite({ enableFirecrawl: false })
expect(screen.queryByText(/Firecrawl/)).not.toBeInTheDocument()
})
it('should show WaterCrawl button when ENABLE_WEBSITE_WATERCRAWL is true', () => {
renderWebsite({ enableWatercrawl: true })
expect(screen.getByText('WaterCrawl')).toBeInTheDocument()
})
it('should not show WaterCrawl button when ENABLE_WEBSITE_WATERCRAWL is false', () => {
renderWebsite({ enableWatercrawl: false })
expect(screen.queryByText('WaterCrawl')).not.toBeInTheDocument()
})
})
describe('Provider Selection', () => {
it('should select Jina Reader by default', () => {
const authedDataSourceList = [createMockDataSourceAuth('jinareader')]
renderWebsite({ authedDataSourceList })
expect(screen.getByTestId('jina-reader-component')).toBeInTheDocument()
})
it('should switch to Firecrawl when Firecrawl button clicked', () => {
const authedDataSourceList = [
createMockDataSourceAuth('jinareader'),
createMockDataSourceAuth('firecrawl'),
]
renderWebsite({ authedDataSourceList })
const firecrawlButton = screen.getByText(/Firecrawl/)
fireEvent.click(firecrawlButton)
expect(screen.getByTestId('firecrawl-component')).toBeInTheDocument()
expect(screen.queryByTestId('jina-reader-component')).not.toBeInTheDocument()
})
it('should switch to WaterCrawl when WaterCrawl button clicked', () => {
const authedDataSourceList = [
createMockDataSourceAuth('jinareader'),
createMockDataSourceAuth('watercrawl'),
]
renderWebsite({ authedDataSourceList })
const watercrawlButton = screen.getByText('WaterCrawl')
fireEvent.click(watercrawlButton)
expect(screen.getByTestId('watercrawl-component')).toBeInTheDocument()
expect(screen.queryByTestId('jina-reader-component')).not.toBeInTheDocument()
})
it('should call onCrawlProviderChange when provider switched', () => {
const authedDataSourceList = [
createMockDataSourceAuth('jinareader'),
createMockDataSourceAuth('firecrawl'),
]
const { props } = renderWebsite({ authedDataSourceList })
const firecrawlButton = screen.getByText(/Firecrawl/)
fireEvent.click(firecrawlButton)
expect(props.onCrawlProviderChange).toHaveBeenCalledWith('firecrawl')
})
})
describe('Provider Content', () => {
it('should show JinaReader component when selected and available', () => {
const authedDataSourceList = [createMockDataSourceAuth('jinareader')]
renderWebsite({ authedDataSourceList })
expect(screen.getByTestId('jina-reader-component')).toBeInTheDocument()
})
it('should show Firecrawl component when selected and available', () => {
const authedDataSourceList = [
createMockDataSourceAuth('jinareader'),
createMockDataSourceAuth('firecrawl'),
]
renderWebsite({ authedDataSourceList })
const firecrawlButton = screen.getByText(/Firecrawl/)
fireEvent.click(firecrawlButton)
expect(screen.getByTestId('firecrawl-component')).toBeInTheDocument()
})
it('should show NoData when selected provider has no credentials', () => {
const authedDataSourceList = [createMockDataSourceAuth('jinareader', 0)]
renderWebsite({ authedDataSourceList })
expect(screen.getByTestId('no-data-component')).toBeInTheDocument()
})
it('should show NoData when no data source available for selected provider', () => {
renderWebsite({ authedDataSourceList: [] })
expect(screen.getByTestId('no-data-component')).toBeInTheDocument()
})
})
describe('NoData Config', () => {
it('should call setShowAccountSettingModal when NoData onConfig is triggered', () => {
renderWebsite({ authedDataSourceList: [] })
const configButton = screen.getByTestId('no-data-config-button')
fireEvent.click(configButton)
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: 'data-source',
})
})
})
describe('Edge Cases', () => {
it('should handle no providers enabled', () => {
renderWebsite({
enableJina: false,
enableFirecrawl: false,
enableWatercrawl: false,
})
expect(screen.queryByText('Jina Reader')).not.toBeInTheDocument()
expect(screen.queryByText(/Firecrawl/)).not.toBeInTheDocument()
expect(screen.queryByText('WaterCrawl')).not.toBeInTheDocument()
})
it('should handle only one provider enabled', () => {
renderWebsite({
enableJina: true,
enableFirecrawl: false,
enableWatercrawl: false,
})
expect(screen.getByText('Jina Reader')).toBeInTheDocument()
expect(screen.queryByText(/Firecrawl/)).not.toBeInTheDocument()
expect(screen.queryByText('WaterCrawl')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,185 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DataSourceProvider } from '@/models/common'
import NoData from '../no-data'
// Mock Setup
// Mock CSS module
vi.mock('../index.module.css', () => ({
default: {
jinaLogo: 'jinaLogo',
watercrawlLogo: 'watercrawlLogo',
},
}))
// Feature flags - default all enabled
let mockEnableFirecrawl = true
let mockEnableJinaReader = true
let mockEnableWaterCrawl = true
vi.mock('@/config', () => ({
get ENABLE_WEBSITE_FIRECRAWL() { return mockEnableFirecrawl },
get ENABLE_WEBSITE_JINAREADER() { return mockEnableJinaReader },
get ENABLE_WEBSITE_WATERCRAWL() { return mockEnableWaterCrawl },
}))
// NoData Component Tests
describe('NoData', () => {
const mockOnConfig = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
mockEnableFirecrawl = true
mockEnableJinaReader = true
mockEnableWaterCrawl = true
})
// Rendering Tests - Per Provider
describe('Rendering per provider', () => {
it('should render fireCrawl provider with emoji and not-configured message', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
expect(screen.getByText('🔥')).toBeInTheDocument()
const titleAndDesc = screen.getAllByText(/fireCrawlNotConfigured/i)
expect(titleAndDesc).toHaveLength(2)
})
it('should render jinaReader provider with jina logo and not-configured message', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
const titleAndDesc = screen.getAllByText(/jinaReaderNotConfigured/i)
expect(titleAndDesc).toHaveLength(2)
})
it('should render waterCrawl provider with emoji and not-configured message', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />)
expect(screen.getByText('💧')).toBeInTheDocument()
const titleAndDesc = screen.getAllByText(/waterCrawlNotConfigured/i)
expect(titleAndDesc).toHaveLength(2)
})
it('should render configure button for each provider', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
expect(screen.getByRole('button', { name: /configure/i })).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onConfig when configure button is clicked', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
fireEvent.click(screen.getByRole('button', { name: /configure/i }))
expect(mockOnConfig).toHaveBeenCalledTimes(1)
})
it('should call onConfig for jinaReader provider', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
fireEvent.click(screen.getByRole('button', { name: /configure/i }))
expect(mockOnConfig).toHaveBeenCalledTimes(1)
})
it('should call onConfig for waterCrawl provider', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />)
fireEvent.click(screen.getByRole('button', { name: /configure/i }))
expect(mockOnConfig).toHaveBeenCalledTimes(1)
})
})
// Feature Flag Disabled - Returns null
describe('Disabled providers (feature flag off)', () => {
it('should fall back to jinaReader when fireCrawl is disabled but jinaReader enabled', () => {
// Arrange — fireCrawl config is null, falls back to providerConfig.jinareader
mockEnableFirecrawl = false
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />,
)
// Assert — renders the jinaReader fallback (not null)
expect(container.innerHTML).not.toBe('')
expect(screen.getAllByText(/jinaReaderNotConfigured/).length).toBeGreaterThan(0)
})
it('should return null when jinaReader is disabled', () => {
// Arrange — jinaReader is the only provider without a fallback
mockEnableJinaReader = false
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />,
)
expect(container.innerHTML).toBe('')
})
it('should fall back to jinaReader when waterCrawl is disabled but jinaReader enabled', () => {
// Arrange — waterCrawl config is null, falls back to providerConfig.jinareader
mockEnableWaterCrawl = false
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />,
)
// Assert — renders the jinaReader fallback (not null)
expect(container.innerHTML).not.toBe('')
expect(screen.getAllByText(/jinaReaderNotConfigured/).length).toBeGreaterThan(0)
})
})
// Fallback behavior
describe('Fallback behavior', () => {
it('should fall back to jinaReader config for unknown provider value', () => {
// Arrange - the || fallback goes to providerConfig.jinareader
// Since DataSourceProvider only has 3 values, we test the fallback
// by checking that jinaReader is the fallback when provider doesn't match
mockEnableJinaReader = true
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
expect(screen.getAllByText(/jinaReaderNotConfigured/i).length).toBeGreaterThan(0)
})
})
describe('Edge Cases', () => {
it('should not call onConfig without user interaction', () => {
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
expect(mockOnConfig).not.toHaveBeenCalled()
})
it('should render correctly when all providers are enabled', () => {
// Arrange - all flags are true by default
const { rerender } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />,
)
expect(screen.getByText('🔥')).toBeInTheDocument()
rerender(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
expect(screen.getAllByText(/jinaReaderNotConfigured/i).length).toBeGreaterThan(0)
rerender(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />)
expect(screen.getByText('💧')).toBeInTheDocument()
})
it('should return null when all providers are disabled and fireCrawl is selected', () => {
mockEnableFirecrawl = false
mockEnableJinaReader = false
mockEnableWaterCrawl = false
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />,
)
expect(container.innerHTML).toBe('')
})
})
})

View File

@ -0,0 +1,197 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import WebsitePreview from '../preview'
// Mock Setup
// Mock the CSS module import - returns class names as-is
vi.mock('../../file-preview/index.module.css', () => ({
default: {
filePreview: 'filePreview',
previewHeader: 'previewHeader',
title: 'title',
previewContent: 'previewContent',
fileContent: 'fileContent',
},
}))
// Test Data Factory
const createPayload = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page Title',
markdown: 'This is **markdown** content',
description: 'A test description',
source_url: 'https://example.com/page',
...overrides,
})
// WebsitePreview Component Tests
describe('WebsitePreview', () => {
const mockHidePreview = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render without crashing', () => {
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should render the page preview header text', () => {
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - i18n returns the key path
expect(screen.getByText(/pagePreview/i)).toBeInTheDocument()
})
it('should render the payload title', () => {
const payload = createPayload({ title: 'My Custom Page' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText('My Custom Page')).toBeInTheDocument()
})
it('should render the payload source_url', () => {
const payload = createPayload({ source_url: 'https://docs.dify.ai/intro' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
const urlElement = screen.getByText('https://docs.dify.ai/intro')
expect(urlElement).toBeInTheDocument()
expect(urlElement).toHaveAttribute('title', 'https://docs.dify.ai/intro')
})
it('should render the payload markdown content', () => {
const payload = createPayload({ markdown: 'Hello world markdown' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText('Hello world markdown')).toBeInTheDocument()
})
it('should render the close button (XMarkIcon)', () => {
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - the close button container is a div with cursor-pointer
const closeButton = screen.getByText(/pagePreview/i).parentElement?.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call hidePreview when close button is clicked', () => {
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Act - find the close button div with cursor-pointer class
const closeButton = screen.getByText(/pagePreview/i)
.closest('[class*="title"]')!
.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
expect(mockHidePreview).toHaveBeenCalledTimes(1)
})
it('should call hidePreview exactly once per click', () => {
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
const closeButton = screen.getByText(/pagePreview/i)
.closest('[class*="title"]')!
.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
fireEvent.click(closeButton)
expect(mockHidePreview).toHaveBeenCalledTimes(2)
})
})
// Props Display Tests
describe('Props Display', () => {
it('should display all payload fields simultaneously', () => {
const payload = createPayload({
title: 'Full Title',
source_url: 'https://full.example.com',
markdown: 'Full markdown text',
})
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText('Full Title')).toBeInTheDocument()
expect(screen.getByText('https://full.example.com')).toBeInTheDocument()
expect(screen.getByText('Full markdown text')).toBeInTheDocument()
})
})
describe('Edge Cases', () => {
it('should render with empty title', () => {
const payload = createPayload({ title: '' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - component still renders, url is visible
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
})
it('should render with empty markdown', () => {
const payload = createPayload({ markdown: '' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should render with empty source_url', () => {
const payload = createPayload({ source_url: '' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should render with very long content', () => {
const longMarkdown = 'A'.repeat(5000)
const payload = createPayload({ markdown: longMarkdown })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
expect(screen.getByText(longMarkdown)).toBeInTheDocument()
})
it('should render with special characters in title', () => {
const payload = createPayload({ title: '<script>alert("xss")</script>' })
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - React escapes HTML by default
expect(screen.getByText('<script>alert("xss")</script>')).toBeInTheDocument()
})
})
// CSS Module Classes
describe('CSS Module Classes', () => {
it('should apply filePreview class to root container', () => {
const payload = createPayload()
const { container } = render(
<WebsitePreview payload={payload} hidePreview={mockHidePreview} />,
)
const root = container.firstElementChild
expect(root?.className).toContain('filePreview')
expect(root?.className).toContain('h-full')
})
})
})

View File

@ -0,0 +1,43 @@
import { fireEvent, 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/tooltip', () => ({
default: ({ popupContent }: { popupContent?: React.ReactNode }) => <div data-testid="tooltip">{popupContent}</div>,
}))
describe('CheckboxWithLabel', () => {
const onChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render label', () => {
render(<CheckboxWithLabel isChecked={false} onChange={onChange} label="Accept terms" />)
expect(screen.getByText('Accept terms')).toBeInTheDocument()
})
it('should render tooltip when provided', () => {
render(
<CheckboxWithLabel
isChecked={false}
onChange={onChange}
label="Option"
tooltip="Help text"
/>,
)
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
})
it('should not render tooltip when not provided', () => {
render(<CheckboxWithLabel isChecked={false} onChange={onChange} label="Option" />)
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument()
})
it('should toggle checked state on checkbox click', () => {
render(<CheckboxWithLabel isChecked={false} onChange={onChange} label="Toggle" testId="my-check" />)
fireEvent.click(screen.getByTestId('checkbox-my-check'))
expect(onChange).toHaveBeenCalledWith(true)
})
})

View File

@ -0,0 +1,43 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CrawledResultItem from '../crawled-result-item'
describe('CrawledResultItem', () => {
const defaultProps = {
payload: { title: 'Example Page', source_url: 'https://example.com/page' } as CrawlResultItemType,
isChecked: false,
isPreview: false,
onCheckChange: vi.fn(),
onPreview: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should render title and url', () => {
render(<CrawledResultItem {...defaultProps} />)
expect(screen.getByText('Example Page')).toBeInTheDocument()
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
})
it('should apply active styling when isPreview', () => {
const { container } = render(<CrawledResultItem {...defaultProps} isPreview={true} />)
expect((container.firstChild as HTMLElement).className).toContain('bg-state-base-active')
})
it('should call onCheckChange with true when unchecked checkbox is clicked', () => {
render(<CrawledResultItem {...defaultProps} isChecked={false} testId="crawl-item" />)
const checkbox = screen.getByTestId('checkbox-crawl-item')
fireEvent.click(checkbox)
expect(defaultProps.onCheckChange).toHaveBeenCalledWith(true)
})
it('should call onCheckChange with false when checked checkbox is clicked', () => {
render(<CrawledResultItem {...defaultProps} isChecked={true} testId="crawl-item" />)
const checkbox = screen.getByTestId('checkbox-crawl-item')
fireEvent.click(checkbox)
expect(defaultProps.onCheckChange).toHaveBeenCalledWith(false)
})
})

View File

@ -0,0 +1,313 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CrawledResult from '../crawled-result'
vi.mock('../checkbox-with-label', () => ({
default: ({ isChecked, onChange, label, testId }: {
isChecked: boolean
onChange: (checked: boolean) => void
label: string
testId?: string
}) => (
<label data-testid={testId}>
<input
type="checkbox"
checked={isChecked}
onChange={() => onChange(!isChecked)}
data-testid={`checkbox-${testId}`}
/>
<span>{label}</span>
</label>
),
}))
vi.mock('../crawled-result-item', () => ({
default: ({ payload, isChecked, isPreview, onCheckChange, onPreview, testId }: {
payload: CrawlResultItem
isChecked: boolean
isPreview: boolean
onCheckChange: (checked: boolean) => void
onPreview: () => void
testId?: string
}) => (
<div data-testid={testId} data-preview={isPreview}>
<input
type="checkbox"
checked={isChecked}
onChange={() => onCheckChange(!isChecked)}
data-testid={`check-${testId}`}
/>
<span>{payload.title}</span>
<span>{payload.source_url}</span>
<button onClick={onPreview} data-testid={`preview-${testId}`}>Preview</button>
</div>
),
}))
const createMockItem = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page',
markdown: '# Test',
description: 'A test page',
source_url: 'https://example.com',
...overrides,
})
const createMockList = (): CrawlResultItem[] => [
createMockItem({ title: 'Page 1', source_url: 'https://example.com/1' }),
createMockItem({ title: 'Page 2', source_url: 'https://example.com/2' }),
createMockItem({ title: 'Page 3', source_url: 'https://example.com/3' }),
]
describe('CrawledResult', () => {
const mockOnSelectedChange = vi.fn()
const mockOnPreview = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render select all checkbox', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
expect(screen.getByTestId('select-all')).toBeInTheDocument()
})
it('should render all items from list', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
expect(screen.getByTestId('item-0')).toBeInTheDocument()
expect(screen.getByTestId('item-1')).toBeInTheDocument()
expect(screen.getByTestId('item-2')).toBeInTheDocument()
})
it('should render scrap time info', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
expect(screen.getByText(/scrapTimeInfo/i)).toBeInTheDocument()
})
it('should apply custom className', () => {
const list = createMockList()
const { container } = render(
<CrawledResult
className="custom-class"
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const rootElement = container.firstChild as HTMLElement
expect(rootElement).toHaveClass('custom-class')
})
})
describe('Select All', () => {
it('should call onSelectedChange with full list when not all checked', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[list[0]]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const selectAllCheckbox = screen.getByTestId('checkbox-select-all')
fireEvent.click(selectAllCheckbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith(list)
})
it('should call onSelectedChange with empty array when all checked', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={list}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const selectAllCheckbox = screen.getByTestId('checkbox-select-all')
fireEvent.click(selectAllCheckbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith([])
})
it('should show selectAll label when not all checked', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[list[0]]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
expect(screen.getByText(/selectAll/i)).toBeInTheDocument()
})
it('should show resetAll label when all checked', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={list}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
expect(screen.getByText(/resetAll/i)).toBeInTheDocument()
})
})
describe('Individual Item Check', () => {
it('should call onSelectedChange with added item when checking', () => {
const list = createMockList()
const checkedList = [list[0]]
render(
<CrawledResult
list={list}
checkedList={checkedList}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const item1Checkbox = screen.getByTestId('check-item-1')
fireEvent.click(item1Checkbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[0], list[1]])
})
it('should call onSelectedChange with removed item when unchecking', () => {
const list = createMockList()
const checkedList = [list[0], list[1]]
render(
<CrawledResult
list={list}
checkedList={checkedList}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const item0Checkbox = screen.getByTestId('check-item-0')
fireEvent.click(item0Checkbox)
expect(mockOnSelectedChange).toHaveBeenCalledWith([list[1]])
})
})
describe('Preview', () => {
it('should call onPreview with correct item when preview clicked', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const previewButton = screen.getByTestId('preview-item-1')
fireEvent.click(previewButton)
expect(mockOnPreview).toHaveBeenCalledWith(list[1])
})
it('should update preview state when preview button is clicked', () => {
const list = createMockList()
render(
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={1.5}
/>,
)
const previewButton = screen.getByTestId('preview-item-0')
fireEvent.click(previewButton)
const item0 = screen.getByTestId('item-0')
expect(item0).toHaveAttribute('data-preview', 'true')
})
})
describe('Edge Cases', () => {
it('should render empty list without crashing', () => {
render(
<CrawledResult
list={[]}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={0}
/>,
)
expect(screen.getByTestId('select-all')).toBeInTheDocument()
})
it('should handle single item list', () => {
const list = [createMockItem()]
render(
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={mockOnSelectedChange}
onPreview={mockOnPreview}
usedTime={0.5}
/>,
)
expect(screen.getByTestId('item-0')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import Crawling from '../crawling'
vi.mock('@/app/components/base/icons/src/public/other', () => ({
RowStruct: (props: React.HTMLAttributes<HTMLDivElement>) => <div data-testid="row-struct" {...props} />,
}))
describe('Crawling', () => {
it('should render crawled count and total', () => {
render(<Crawling crawledNum={3} totalNum={10} />)
expect(screen.getByText(/3/)).toBeInTheDocument()
expect(screen.getByText(/10/)).toBeInTheDocument()
})
it('should render skeleton rows', () => {
render(<Crawling crawledNum={0} totalNum={5} />)
expect(screen.getAllByTestId('row-struct')).toHaveLength(4)
})
})

View File

@ -0,0 +1,29 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ErrorMessage from '../error-message'
vi.mock('@/app/components/base/icons/src/vender/solid/alertsAndFeedback', () => ({
AlertTriangle: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="alert-icon" {...props} />,
}))
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', () => {
render(<ErrorMessage title="Error" />)
expect(screen.queryByText('Detailed error info')).not.toBeInTheDocument()
})
it('should render alert icon', () => {
render(<ErrorMessage title="Error" />)
expect(screen.getByTestId('alert-icon')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,46 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Field from '../field'
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ popupContent }: { popupContent?: React.ReactNode }) => <div data-testid="tooltip">{popupContent}</div>,
}))
describe('WebsiteField', () => {
const onChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render label', () => {
render(<Field label="URL" value="" onChange={onChange} />)
expect(screen.getByText('URL')).toBeInTheDocument()
})
it('should render required asterisk when isRequired', () => {
render(<Field label="URL" value="" onChange={onChange} isRequired />)
expect(screen.getByText('*')).toBeInTheDocument()
})
it('should not render required asterisk by default', () => {
render(<Field label="URL" value="" onChange={onChange} />)
expect(screen.queryByText('*')).not.toBeInTheDocument()
})
it('should render tooltip when provided', () => {
render(<Field label="URL" value="" onChange={onChange} tooltip="Enter full URL" />)
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
})
it('should pass value and onChange to Input', () => {
render(<Field label="URL" value="https://example.com" onChange={onChange} />)
expect(screen.getByDisplayValue('https://example.com')).toBeInTheDocument()
})
it('should call onChange when input changes', () => {
render(<Field label="URL" value="" onChange={onChange} />)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'new' } })
expect(onChange).toHaveBeenCalledWith('new')
})
})

View File

@ -0,0 +1,45 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Header from '../header'
describe('WebsiteHeader', () => {
const defaultProps = {
title: 'Jina Reader',
docTitle: 'Documentation',
docLink: 'https://docs.example.com',
onClickConfiguration: vi.fn(),
buttonText: 'Config',
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should render title', () => {
render(<Header {...defaultProps} />)
expect(screen.getByText('Jina Reader')).toBeInTheDocument()
})
it('should render doc link with correct href', () => {
render(<Header {...defaultProps} />)
const link = screen.getByText('Documentation').closest('a')
expect(link).toHaveAttribute('href', 'https://docs.example.com')
expect(link).toHaveAttribute('target', '_blank')
})
it('should render configuration button with text when not in pipeline', () => {
render(<Header {...defaultProps} />)
expect(screen.getByText('Config')).toBeInTheDocument()
})
it('should call onClickConfiguration on button click', () => {
render(<Header {...defaultProps} />)
fireEvent.click(screen.getByText('Config').closest('button')!)
expect(defaultProps.onClickConfiguration).toHaveBeenCalledOnce()
})
it('should hide button text when isInPipeline', () => {
render(<Header {...defaultProps} isInPipeline={true} />)
expect(screen.queryByText('Config')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,52 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Input from '../input'
describe('WebsiteInput', () => {
const onChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render text input by default', () => {
render(<Input value="hello" onChange={onChange} />)
const input = screen.getByDisplayValue('hello')
expect(input).toHaveAttribute('type', 'text')
})
it('should render number input when isNumber is true', () => {
render(<Input value={42} onChange={onChange} isNumber />)
const input = screen.getByDisplayValue('42')
expect(input).toHaveAttribute('type', 'number')
})
it('should call onChange with string value for text input', () => {
render(<Input value="" onChange={onChange} />)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'new value' } })
expect(onChange).toHaveBeenCalledWith('new value')
})
it('should call onChange with parsed integer for number input', () => {
render(<Input value={0} onChange={onChange} isNumber />)
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '10' } })
expect(onChange).toHaveBeenCalledWith(10)
})
it('should call onChange with empty string for NaN number input', () => {
render(<Input value={0} onChange={onChange} isNumber />)
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: 'abc' } })
expect(onChange).toHaveBeenCalledWith('')
})
it('should clamp negative numbers to 0', () => {
render(<Input value={0} onChange={onChange} isNumber />)
fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '-5' } })
expect(onChange).toHaveBeenCalledWith(0)
})
it('should render placeholder', () => {
render(<Input value="" onChange={onChange} placeholder="Enter URL" />)
expect(screen.getByPlaceholderText('Enter URL')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,43 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import OptionsWrap from '../options-wrap'
vi.mock('@/app/components/base/icons/src/vender/line/arrows', () => ({
ChevronRight: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="chevron-icon" {...props} />,
}))
describe('OptionsWrap', () => {
it('should render children when not folded', () => {
render(
<OptionsWrap>
<div data-testid="child-content">Options here</div>
</OptionsWrap>,
)
expect(screen.getByTestId('child-content')).toBeInTheDocument()
})
it('should toggle fold on click', () => {
render(
<OptionsWrap>
<div data-testid="child-content">Options here</div>
</OptionsWrap>,
)
// Initially visible
expect(screen.getByTestId('child-content')).toBeInTheDocument()
fireEvent.click(screen.getByText('datasetCreation.stepOne.website.options'))
expect(screen.queryByTestId('child-content')).not.toBeInTheDocument()
fireEvent.click(screen.getByText('datasetCreation.stepOne.website.options'))
expect(screen.getByTestId('child-content')).toBeInTheDocument()
})
it('should render options label', () => {
render(
<OptionsWrap>
<div>Content</div>
</OptionsWrap>,
)
expect(screen.getByText('datasetCreation.stepOne.website.options')).toBeInTheDocument()
})
})

View File

@ -2,24 +2,18 @@ import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// ============================================================================
// Component Imports (after mocks)
// ============================================================================
import UrlInput from './url-input'
import UrlInput from '../url-input'
// ============================================================================
// Mock Setup
// ============================================================================
// Mock useDocLink hook
vi.mock('@/context/i18n', () => ({
useDocLink: vi.fn(() => () => 'https://docs.example.com'),
}))
// ============================================================================
// UrlInput Component Tests
// ============================================================================
describe('UrlInput', () => {
const mockOnRun = vi.fn()
@ -28,9 +22,6 @@ describe('UrlInput', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
@ -71,9 +62,6 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should update input value when user types', async () => {
const user = userEvent.setup()
@ -146,9 +134,7 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Props Variations Tests
// --------------------------------------------------------------------------
describe('Props Variations', () => {
it('should update button state when isRunning changes from false to true', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
@ -190,9 +176,6 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle special characters in url', async () => {
const user = userEvent.setup()
@ -272,9 +255,7 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// handleOnRun Branch Coverage Tests
// --------------------------------------------------------------------------
describe('handleOnRun Branch Coverage', () => {
it('should return early when isRunning is true (branch: isRunning = true)', async () => {
const user = userEvent.setup()
@ -307,9 +288,7 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Button Text Branch Coverage Tests
// --------------------------------------------------------------------------
describe('Button Text Branch Coverage', () => {
it('should display run text when isRunning is false (branch: !isRunning = true)', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
@ -328,9 +307,6 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
@ -368,9 +344,6 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Integration Tests
// --------------------------------------------------------------------------
describe('Integration', () => {
it('should complete full workflow: type url -> click run -> verify callback', async () => {
const user = userEvent.setup()
@ -381,7 +354,6 @@ describe('UrlInput', () => {
const input = screen.getByRole('textbox')
await user.type(input, 'https://mywebsite.com')
// Click run
const button = screen.getByRole('button')
await user.click(button)

View File

@ -3,15 +3,11 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// ============================================================================
// Component Import (after mocks)
// ============================================================================
import FireCrawl from './index'
import FireCrawl from '../index'
// ============================================================================
// Mock Setup - Only mock API calls and context
// ============================================================================
// Mock API service
const mockCreateFirecrawlTask = vi.fn()
@ -38,9 +34,7 @@ vi.mock('@/context/i18n', () => ({
useDocLink: vi.fn(() => () => 'https://docs.example.com'),
}))
// ============================================================================
// Test Data Factory
// ============================================================================
const createMockCrawlOptions = (overrides: Partial<CrawlOptions> = {}): CrawlOptions => ({
crawl_sub_pages: true,
@ -61,9 +55,7 @@ const createMockCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): Cr
...overrides,
})
// ============================================================================
// FireCrawl Component Tests
// ============================================================================
describe('FireCrawl', () => {
const mockOnPreview = vi.fn()
@ -91,9 +83,6 @@ describe('FireCrawl', () => {
return screen.getByPlaceholderText('https://docs.example.com')
}
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<FireCrawl {...defaultProps} />)
@ -131,9 +120,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Configuration Button Tests
// --------------------------------------------------------------------------
describe('Configuration Button', () => {
it('should call setShowAccountSettingModal when configure button is clicked', async () => {
const user = userEvent.setup()
@ -148,9 +135,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// URL Validation Tests
// --------------------------------------------------------------------------
describe('URL Validation', () => {
it('should show error toast when URL is empty', async () => {
const user = userEvent.setup()
@ -261,9 +246,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Crawl Execution Tests
// --------------------------------------------------------------------------
describe('Crawl Execution', () => {
it('should call createFirecrawlTask with correct parameters', async () => {
const user = userEvent.setup()
@ -372,9 +355,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Crawl Status Polling Tests
// --------------------------------------------------------------------------
describe('Crawl Status Polling', () => {
it('should handle completed status', async () => {
const user = userEvent.setup()
@ -508,9 +489,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Error Handling Tests
// --------------------------------------------------------------------------
describe('Error Handling', () => {
it('should handle API exception during task creation', async () => {
const user = userEvent.setup()
@ -594,9 +573,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Options Change Tests
// --------------------------------------------------------------------------
describe('Options Change', () => {
it('should call onCrawlOptionsChange when options change', () => {
render(<FireCrawl {...defaultProps} />)
@ -623,9 +600,7 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Crawled Result Display Tests
// --------------------------------------------------------------------------
describe('Crawled Result Display', () => {
it('should display CrawledResult when crawl is finished successfully', async () => {
const user = userEvent.setup()
@ -686,9 +661,6 @@ describe('FireCrawl', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<FireCrawl {...defaultProps} />)

View File

@ -1,11 +1,9 @@
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'
import Options from '../options'
// ============================================================================
// Test Data Factory
// ============================================================================
const createMockCrawlOptions = (overrides: Partial<CrawlOptions> = {}): CrawlOptions => ({
crawl_sub_pages: true,
@ -18,9 +16,7 @@ const createMockCrawlOptions = (overrides: Partial<CrawlOptions> = {}): CrawlOpt
...overrides,
})
// ============================================================================
// Options Component Tests
// ============================================================================
describe('Options', () => {
const mockOnChange = vi.fn()
@ -34,9 +30,6 @@ describe('Options', () => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
const payload = createMockCrawlOptions()
@ -107,9 +100,7 @@ describe('Options', () => {
})
})
// --------------------------------------------------------------------------
// 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 })
@ -180,9 +171,6 @@ describe('Options', () => {
})
})
// --------------------------------------------------------------------------
// 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 })
@ -263,9 +251,6 @@ describe('Options', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle empty string values', () => {
const payload = createMockCrawlOptions({
@ -340,9 +325,7 @@ describe('Options', () => {
})
})
// --------------------------------------------------------------------------
// handleChange Callback Tests
// --------------------------------------------------------------------------
describe('handleChange Callback', () => {
it('should create a new callback for each key', () => {
const payload = createMockCrawlOptions()
@ -378,9 +361,6 @@ describe('Options', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const payload = createMockCrawlOptions()

View File

@ -1,15 +1,13 @@
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import UrlInput from './base/url-input'
import UrlInput from '../base/url-input'
// Mock doc link context
vi.mock('@/context/i18n', () => ({
useDocLink: () => () => 'https://docs.example.com',
}))
// ============================================================================
// UrlInput Component Tests
// ============================================================================
describe('UrlInput', () => {
beforeEach(() => {
@ -23,50 +21,36 @@ describe('UrlInput', () => {
...overrides,
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const props = createUrlInputProps()
// Act
render(<UrlInput {...props} />)
// Assert
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByRole('button', { name: /run/i })).toBeInTheDocument()
})
it('should render input with placeholder from docLink', () => {
// Arrange
const props = createUrlInputProps()
// Act
render(<UrlInput {...props} />)
// Assert
const input = screen.getByRole('textbox')
expect(input).toHaveAttribute('placeholder', 'https://docs.example.com')
})
it('should render run button with correct text when not running', () => {
// Arrange
const props = createUrlInputProps({ isRunning: false })
// Act
render(<UrlInput {...props} />)
// Assert
expect(screen.getByRole('button', { name: /run/i })).toBeInTheDocument()
})
it('should render button without text when running', () => {
// Arrange
const props = createUrlInputProps({ isRunning: true })
// Act
render(<UrlInput {...props} />)
// Assert - find button by data-testid when in loading state
@ -77,11 +61,9 @@ describe('UrlInput', () => {
})
it('should show loading state on button when running', () => {
// Arrange
const onRun = vi.fn()
const props = createUrlInputProps({ isRunning: true, onRun })
// Act
render(<UrlInput {...props} />)
// Assert - find button by data-testid when in loading state
@ -97,100 +79,77 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// User Input Tests
// --------------------------------------------------------------------------
describe('User Input', () => {
it('should update URL value when user types', async () => {
// Arrange
const props = createUrlInputProps()
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://test.com')
// Assert
expect(input).toHaveValue('https://test.com')
})
it('should handle URL input clearing', async () => {
// Arrange
const props = createUrlInputProps()
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://test.com')
await userEvent.clear(input)
// Assert
expect(input).toHaveValue('')
})
it('should handle special characters in URL', async () => {
// Arrange
const props = createUrlInputProps()
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://example.com/path?query=value&foo=bar')
// Assert
expect(input).toHaveValue('https://example.com/path?query=value&foo=bar')
})
})
// --------------------------------------------------------------------------
// Button Click Tests
// --------------------------------------------------------------------------
describe('Button Click', () => {
it('should call onRun with URL when button is clicked', async () => {
// Arrange
const onRun = vi.fn()
const props = createUrlInputProps({ onRun })
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://run-test.com')
await userEvent.click(screen.getByRole('button', { name: /run/i }))
// Assert
expect(onRun).toHaveBeenCalledWith('https://run-test.com')
expect(onRun).toHaveBeenCalledTimes(1)
})
it('should call onRun with empty string if no URL entered', async () => {
// Arrange
const onRun = vi.fn()
const props = createUrlInputProps({ onRun })
// Act
render(<UrlInput {...props} />)
await userEvent.click(screen.getByRole('button', { name: /run/i }))
// Assert
expect(onRun).toHaveBeenCalledWith('')
})
it('should not call onRun when isRunning is true', async () => {
// Arrange
const onRun = vi.fn()
const props = createUrlInputProps({ onRun, isRunning: true })
// Act
render(<UrlInput {...props} />)
const runButton = screen.getByTestId('url-input-run-button')
fireEvent.click(runButton)
// Assert
expect(onRun).not.toHaveBeenCalled()
})
it('should not call onRun when already running', async () => {
// Arrange
const onRun = vi.fn()
// First render with isRunning=false, type URL, then rerender with isRunning=true
@ -210,31 +169,24 @@ describe('UrlInput', () => {
})
it('should prevent multiple clicks when already running', async () => {
// Arrange
const onRun = vi.fn()
const props = createUrlInputProps({ onRun, isRunning: true })
// Act
render(<UrlInput {...props} />)
const runButton = screen.getByTestId('url-input-run-button')
fireEvent.click(runButton)
fireEvent.click(runButton)
fireEvent.click(runButton)
// Assert
expect(onRun).not.toHaveBeenCalled()
})
})
// --------------------------------------------------------------------------
// Props Tests
// --------------------------------------------------------------------------
describe('Props', () => {
it('should respond to isRunning prop change', () => {
// Arrange
const props = createUrlInputProps({ isRunning: false })
// Act
const { rerender } = render(<UrlInput {...props} />)
expect(screen.getByRole('button', { name: /run/i })).toBeInTheDocument()
@ -249,11 +201,9 @@ describe('UrlInput', () => {
})
it('should call updated onRun callback after prop change', async () => {
// Arrange
const onRun1 = vi.fn()
const onRun2 = vi.fn()
// Act
const { rerender } = render(<UrlInput isRunning={false} onRun={onRun1} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://first.com')
@ -268,15 +218,11 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Callback Stability Tests
// --------------------------------------------------------------------------
describe('Callback Stability', () => {
it('should use memoized handleUrlChange callback', async () => {
// Arrange
const props = createUrlInputProps()
// Act
const { rerender } = render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'a')
@ -290,10 +236,8 @@ describe('UrlInput', () => {
})
it('should maintain URL state across rerenders', async () => {
// Arrange
const props = createUrlInputProps()
// Act
const { rerender } = render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://stable.com')
@ -306,58 +250,43 @@ describe('UrlInput', () => {
})
})
// --------------------------------------------------------------------------
// Component Memoization Tests
// --------------------------------------------------------------------------
describe('Component Memoization', () => {
it('should be wrapped with React.memo', () => {
// Assert
expect(UrlInput.$$typeof).toBeDefined()
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle very long URLs', async () => {
// Arrange
const props = createUrlInputProps()
const longUrl = `https://example.com/${'a'.repeat(1000)}`
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, longUrl)
// Assert
expect(input).toHaveValue(longUrl)
})
it('should handle URLs with unicode characters', async () => {
// Arrange
const props = createUrlInputProps()
const unicodeUrl = 'https://example.com/路径/测试'
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, unicodeUrl)
// Assert
expect(input).toHaveValue(unicodeUrl)
})
it('should handle rapid typing', async () => {
// Arrange
const props = createUrlInputProps()
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://rapid.com', { delay: 1 })
// Assert
expect(input).toHaveValue('https://rapid.com')
})
@ -366,7 +295,6 @@ describe('UrlInput', () => {
const onRun = vi.fn()
const props = createUrlInputProps({ onRun })
// Act
render(<UrlInput {...props} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'https://enter.com')
@ -376,16 +304,13 @@ describe('UrlInput', () => {
button.focus()
await userEvent.keyboard('{Enter}')
// Assert
expect(onRun).toHaveBeenCalledWith('https://enter.com')
})
it('should handle empty URL submission', async () => {
// Arrange
const onRun = vi.fn()
const props = createUrlInputProps({ onRun })
// Act
render(<UrlInput {...props} />)
await userEvent.click(screen.getByRole('button', { name: /run/i }))

View File

@ -0,0 +1,191 @@
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,
})
// Jina Reader Options Component Tests
describe('Options (jina-reader)', () => {
const mockOnChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
const getCheckboxes = (container: HTMLElement) => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
describe('Rendering', () => {
it('should render crawlSubPage and useSitemap checkboxes and limit field', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
expect(screen.getByText(/useSitemap/i)).toBeInTheDocument()
expect(screen.getByText(/limit/i)).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)
})
it('should render limit field with required indicator', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
const requiredIndicator = screen.getByText('*')
expect(requiredIndicator).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')
})
})
// 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)
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)
expect(checkboxes[0].querySelector('svg')).not.toBeInTheDocument()
})
it('should display use_sitemap checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ use_sitemap: true })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes[1].querySelector('svg')).toBeInTheDocument()
})
it('should display use_sitemap checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ use_sitemap: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
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} />)
expect(screen.getByDisplayValue('25')).toBeInTheDocument()
})
})
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 use_sitemap when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ use_sitemap: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[1])
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
use_sitemap: 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,
})
})
})
describe('Edge Cases', () => {
it('should handle zero limit value', () => {
const payload = createMockCrawlOptions({ limit: 0 })
render(<Options payload={payload} onChange={mockOnChange} />)
const zeroInputs = screen.getAllByDisplayValue('0')
expect(zeroInputs.length).toBeGreaterThanOrEqual(1)
})
it('should preserve other payload fields when updating one field', () => {
const payload = createMockCrawlOptions({
crawl_sub_pages: true,
limit: 10,
use_sitemap: true,
})
render(<Options payload={payload} onChange={mockOnChange} />)
const limitInput = screen.getByDisplayValue('10')
fireEvent.change(limitInput, { target: { value: '20' } })
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
limit: 20,
})
})
})
describe('Memoization', () => {
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()
})
})
})

View File

@ -0,0 +1,192 @@
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Component Imports (after mocks)
import UrlInput from '../url-input'
// Mock Setup
vi.mock('@/context/i18n', () => ({
useDocLink: vi.fn(() => () => 'https://docs.example.com'),
}))
// Jina Reader UrlInput Component Tests
describe('UrlInput (jina-reader)', () => {
const mockOnRun = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render input and run button', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render input with placeholder from docLink', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
expect(input).toHaveAttribute('placeholder', 'https://docs.example.com')
})
it('should show run text when not running', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).toHaveTextContent(/run/i)
})
it('should hide run text when running', () => {
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).not.toHaveTextContent(/run/i)
})
it('should show loading state on button when running', () => {
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).toHaveTextContent(/loading/i)
})
it('should not show loading state on button when not running', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).not.toHaveTextContent(/loading/i)
})
})
describe('User Interactions', () => {
it('should update url when user types in input', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://example.com')
expect(input).toHaveValue('https://example.com')
})
it('should call onRun with url when run button clicked and not running', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://example.com')
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith('https://example.com')
expect(mockOnRun).toHaveBeenCalledTimes(1)
})
it('should NOT call onRun when isRunning is true', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'https://example.com' } })
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).not.toHaveBeenCalled()
})
it('should call onRun with empty string when button clicked with empty input', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith('')
})
})
// Props Variations Tests
describe('Props Variations', () => {
it('should update button state when isRunning changes from false to true', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('button')).toHaveTextContent(/run/i)
rerender(<UrlInput isRunning={true} onRun={mockOnRun} />)
expect(screen.getByRole('button')).not.toHaveTextContent(/run/i)
})
it('should preserve input value when isRunning prop changes', async () => {
const user = userEvent.setup()
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://preserved.com')
expect(input).toHaveValue('https://preserved.com')
rerender(<UrlInput isRunning={true} onRun={mockOnRun} />)
expect(input).toHaveValue('https://preserved.com')
})
})
describe('Edge Cases', () => {
it('should handle special characters in url', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
const specialUrl = 'https://example.com/path?query=test&param=value#anchor'
await user.type(input, specialUrl)
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith(specialUrl)
})
it('should handle rapid input changes', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'a' } })
fireEvent.change(input, { target: { value: 'ab' } })
fireEvent.change(input, { target: { value: 'https://final.com' } })
expect(input).toHaveValue('https://final.com')
fireEvent.click(screen.getByRole('button'))
expect(mockOnRun).toHaveBeenCalledWith('https://final.com')
})
})
describe('Integration', () => {
it('should complete full workflow: type url -> click run -> verify callback', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://mywebsite.com')
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith('https://mywebsite.com')
})
it('should show correct states during running workflow', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('button')).toHaveTextContent(/run/i)
rerender(<UrlInput isRunning={true} onRun={mockOnRun} />)
expect(screen.getByRole('button')).not.toHaveTextContent(/run/i)
rerender(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('button')).toHaveTextContent(/run/i)
})
})
})

View File

@ -0,0 +1,276 @@
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,
})
// WaterCrawl Options Component Tests
describe('Options (watercrawl)', () => {
const mockOnChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
const getCheckboxes = (container: HTMLElement) => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
describe('Rendering', () => {
it('should render all form fields', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
expect(screen.getByText(/extractOnlyMainContent/i)).toBeInTheDocument()
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 two checkboxes', () => {
const payload = createMockCrawlOptions()
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes.length).toBe(2)
})
it('should render limit field with required indicator', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
const requiredIndicator = screen.getByText('*')
expect(requiredIndicator).toBeInTheDocument()
})
it('should render placeholder for excludes field', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByPlaceholderText('blog/*, /about/*')).toBeInTheDocument()
})
it('should render placeholder for includes field', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByPlaceholderText('articles/*')).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')
})
})
// 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)
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)
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)
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)
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} />)
expect(screen.getByDisplayValue('25')).toBeInTheDocument()
})
it('should display max_depth value in input', () => {
const payload = createMockCrawlOptions({ max_depth: 5 })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('5')).toBeInTheDocument()
})
it('should display excludes value in input', () => {
const payload = createMockCrawlOptions({ excludes: 'test/*' })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('test/*')).toBeInTheDocument()
})
it('should display includes value in input', () => {
const payload = createMockCrawlOptions({ includes: 'docs/*' })
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('docs/*')).toBeInTheDocument()
})
})
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/*',
})
})
})
describe('Edge Cases', () => {
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,
})
})
it('should handle zero values', () => {
const payload = createMockCrawlOptions({ limit: 0, max_depth: 0 })
render(<Options payload={payload} onChange={mockOnChange} />)
const zeroInputs = screen.getAllByDisplayValue('0')
expect(zeroInputs.length).toBeGreaterThanOrEqual(1)
})
})
describe('Memoization', () => {
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()
})
})
})