mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
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:
@ -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])
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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('')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
|
||||
@ -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} />)
|
||||
@ -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()
|
||||
@ -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 }))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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¶m=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)
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user