mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +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:
31
web/app/components/datasets/create/__tests__/icons.spec.ts
Normal file
31
web/app/components/datasets/create/__tests__/icons.spec.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { indexMethodIcon, retrievalIcon } from '../icons'
|
||||
|
||||
describe('create/icons', () => {
|
||||
// Verify icon map exports have expected keys
|
||||
describe('indexMethodIcon', () => {
|
||||
it('should have high_quality and economical keys', () => {
|
||||
expect(indexMethodIcon).toHaveProperty('high_quality')
|
||||
expect(indexMethodIcon).toHaveProperty('economical')
|
||||
})
|
||||
|
||||
it('should have truthy values for each key', () => {
|
||||
expect(indexMethodIcon.high_quality).toBeTruthy()
|
||||
expect(indexMethodIcon.economical).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('retrievalIcon', () => {
|
||||
it('should have vector, fullText, and hybrid keys', () => {
|
||||
expect(retrievalIcon).toHaveProperty('vector')
|
||||
expect(retrievalIcon).toHaveProperty('fullText')
|
||||
expect(retrievalIcon).toHaveProperty('hybrid')
|
||||
})
|
||||
|
||||
it('should have truthy values for each key', () => {
|
||||
expect(retrievalIcon.vector).toBeTruthy()
|
||||
expect(retrievalIcon.fullText).toBeTruthy()
|
||||
expect(retrievalIcon.hybrid).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,141 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { IndexingStatusResponse } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import IndexingProgressItem from '../indexing-progress-item'
|
||||
|
||||
vi.mock('@/app/components/billing/priority-label', () => ({
|
||||
default: () => <span data-testid="priority-label">Priority</span>,
|
||||
}))
|
||||
vi.mock('../../../common/document-file-icon', () => ({
|
||||
default: ({ name }: { name?: string }) => <span data-testid="file-icon">{name}</span>,
|
||||
}))
|
||||
vi.mock('@/app/components/base/notion-icon', () => ({
|
||||
default: ({ src }: { src?: string }) => <span data-testid="notion-icon">{src}</span>,
|
||||
}))
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children?: ReactNode, popupContent?: ReactNode }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('IndexingProgressItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const makeDetail = (overrides: Partial<IndexingStatusResponse> = {}): IndexingStatusResponse => ({
|
||||
id: 'doc-1',
|
||||
indexing_status: 'indexing',
|
||||
processing_started_at: 0,
|
||||
parsing_completed_at: 0,
|
||||
cleaning_completed_at: 0,
|
||||
splitting_completed_at: 0,
|
||||
completed_at: null,
|
||||
paused_at: null,
|
||||
error: null,
|
||||
stopped_at: null,
|
||||
completed_segments: 50,
|
||||
total_segments: 100,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
it('should render name and progress for embedding status', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail()}
|
||||
name="test.pdf"
|
||||
sourceType={DataSourceType.FILE}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Name appears in both the file-icon mock and the display div; verify at least one
|
||||
expect(screen.getAllByText('test.pdf').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getByText('50%')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render file icon for FILE source type', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail()}
|
||||
name="report.docx"
|
||||
sourceType={DataSourceType.FILE}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('file-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render notion icon for NOTION source type', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail()}
|
||||
name="My Page"
|
||||
sourceType={DataSourceType.NOTION}
|
||||
notionIcon="notion-icon-url"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('notion-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render success icon for completed status', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail({ indexing_status: 'completed' })}
|
||||
name="done.pdf"
|
||||
/>,
|
||||
)
|
||||
|
||||
// No progress percentage should be shown for completed
|
||||
expect(screen.queryByText(/%/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error icon with tooltip for error status', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail({ indexing_status: 'error', error: 'Parse failed' })}
|
||||
name="broken.pdf"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'Parse failed')
|
||||
})
|
||||
|
||||
it('should show priority label when billing is enabled', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail()}
|
||||
name="test.pdf"
|
||||
enableBilling={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('priority-label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show priority label when billing is disabled', () => {
|
||||
render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail()}
|
||||
name="test.pdf"
|
||||
enableBilling={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('priority-label')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply error styling for error status', () => {
|
||||
const { container } = render(
|
||||
<IndexingProgressItem
|
||||
detail={makeDetail({ indexing_status: 'error' })}
|
||||
name="error.pdf"
|
||||
/>,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('bg-state-destructive-hover-alt')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,145 @@
|
||||
import type { ProcessRuleResponse } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ProcessMode } from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import RuleDetail from '../rule-detail'
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/metadata', () => ({
|
||||
FieldInfo: ({ label, displayedValue }: { label: string, displayedValue: string }) => (
|
||||
<div data-testid="field-info">
|
||||
<span data-testid="field-label">{label}</span>
|
||||
<span data-testid="field-value">{displayedValue}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
vi.mock('../../icons', () => ({
|
||||
indexMethodIcon: { economical: '/icons/economical.svg', high_quality: '/icons/hq.svg' },
|
||||
retrievalIcon: { fullText: '/icons/ft.svg', hybrid: '/icons/hy.svg', vector: '/icons/vec.svg' },
|
||||
}))
|
||||
|
||||
describe('RuleDetail', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const makeSourceData = (overrides: Partial<ProcessRuleResponse> = {}): ProcessRuleResponse => ({
|
||||
mode: ProcessMode.general,
|
||||
rules: {
|
||||
segmentation: { separator: '\n', max_tokens: 500, chunk_overlap: 50 },
|
||||
pre_processing_rules: [
|
||||
{ id: 'remove_extra_spaces', enabled: true },
|
||||
{ id: 'remove_urls_emails', enabled: false },
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
} as ProcessRuleResponse)
|
||||
|
||||
it('should render mode, segment length, text cleaning, index mode, and retrieval fields', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData()}
|
||||
indexingType="high_quality"
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
const fieldInfos = screen.getAllByTestId('field-info')
|
||||
// mode, segmentLength, textCleaning, indexMode, retrievalSetting = 5
|
||||
expect(fieldInfos.length).toBe(5)
|
||||
})
|
||||
|
||||
it('should display "custom" for general mode', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData({ mode: ProcessMode.general })}
|
||||
indexingType="high_quality"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
expect(values[0].textContent).toContain('embedding.custom')
|
||||
})
|
||||
|
||||
it('should display hierarchical mode with parent mode label', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData({
|
||||
mode: ProcessMode.parentChild,
|
||||
rules: {
|
||||
parent_mode: 'paragraph',
|
||||
segmentation: { separator: '\n', max_tokens: 1000, chunk_overlap: 50 },
|
||||
subchunk_segmentation: { max_tokens: 200 },
|
||||
pre_processing_rules: [],
|
||||
} as unknown as ProcessRuleResponse['rules'],
|
||||
})}
|
||||
indexingType="high_quality"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
expect(values[0].textContent).toContain('embedding.hierarchical')
|
||||
})
|
||||
|
||||
it('should display "-" when no sourceData mode', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData({ mode: undefined as unknown as ProcessMode })}
|
||||
indexingType="high_quality"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
expect(values[0].textContent).toBe('-')
|
||||
})
|
||||
|
||||
it('should display segment length for general mode', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData()}
|
||||
indexingType="high_quality"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
expect(values[1].textContent).toBe('500')
|
||||
})
|
||||
|
||||
it('should display enabled pre-processing rules', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData()}
|
||||
indexingType="high_quality"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
// Only remove_extra_spaces is enabled
|
||||
expect(values[2].textContent).toContain('stepTwo.removeExtraSpaces')
|
||||
})
|
||||
|
||||
it('should display economical index mode', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData()}
|
||||
indexingType="economy"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
// Index mode field is 4th (index 3)
|
||||
expect(values[3].textContent).toContain('stepTwo.economical')
|
||||
})
|
||||
|
||||
it('should display qualified index mode for high_quality', () => {
|
||||
render(
|
||||
<RuleDetail
|
||||
sourceData={makeSourceData()}
|
||||
indexingType="high_quality"
|
||||
/>,
|
||||
)
|
||||
|
||||
const values = screen.getAllByTestId('field-value')
|
||||
expect(values[3].textContent).toContain('stepTwo.qualified')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,29 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import UpgradeBanner from '../upgrade-banner'
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/solid/general', () => ({
|
||||
ZapFast: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="zap-icon" {...props} />,
|
||||
}))
|
||||
vi.mock('@/app/components/billing/upgrade-btn', () => ({
|
||||
default: ({ loc }: { loc: string }) => <button data-testid="upgrade-btn" data-loc={loc}>Upgrade</button>,
|
||||
}))
|
||||
|
||||
describe('UpgradeBanner', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render the banner with icon, text, and upgrade button', () => {
|
||||
render(<UpgradeBanner />)
|
||||
|
||||
expect(screen.getByTestId('zap-icon')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.plansCommon.documentProcessingPriorityUpgrade')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('upgrade-btn')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass correct loc to UpgradeBtn', () => {
|
||||
render(<UpgradeBanner />)
|
||||
expect(screen.getByTestId('upgrade-btn')).toHaveAttribute('data-loc', 'knowledge-speed-up')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,179 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useIndexingStatusPolling } from '../use-indexing-status-polling'
|
||||
|
||||
const mockFetchIndexingStatusBatch = vi.fn()
|
||||
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
fetchIndexingStatusBatch: (...args: unknown[]) => mockFetchIndexingStatusBatch(...args),
|
||||
}))
|
||||
|
||||
describe('useIndexingStatusPolling', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
const defaultParams = { datasetId: 'ds-1', batchId: 'batch-1' }
|
||||
|
||||
it('should initialize with empty status list', async () => {
|
||||
mockFetchIndexingStatusBatch.mockReturnValue(new Promise(() => {}))
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
|
||||
expect(result.current.statusList).toEqual([])
|
||||
expect(result.current.isEmbedding).toBe(false)
|
||||
expect(result.current.isEmbeddingCompleted).toBe(false)
|
||||
})
|
||||
|
||||
it('should fetch status on mount and update state', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [{ indexing_status: 'indexing', completed_segments: 5, total_segments: 10 }],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
// Flush the resolved promise
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
|
||||
expect(mockFetchIndexingStatusBatch).toHaveBeenCalledWith({
|
||||
datasetId: 'ds-1',
|
||||
batchId: 'batch-1',
|
||||
})
|
||||
expect(result.current.statusList).toHaveLength(1)
|
||||
expect(result.current.isEmbedding).toBe(true)
|
||||
expect(result.current.isEmbeddingCompleted).toBe(false)
|
||||
})
|
||||
|
||||
it('should stop polling when all completed', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [{ indexing_status: 'completed' }],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
|
||||
expect(result.current.isEmbeddingCompleted).toBe(true)
|
||||
expect(result.current.isEmbedding).toBe(false)
|
||||
|
||||
// Should not schedule another poll
|
||||
const callCount = mockFetchIndexingStatusBatch.mock.calls.length
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
})
|
||||
expect(mockFetchIndexingStatusBatch).toHaveBeenCalledTimes(callCount)
|
||||
})
|
||||
|
||||
it('should continue polling on fetch error', async () => {
|
||||
mockFetchIndexingStatusBatch
|
||||
.mockRejectedValueOnce(new Error('network'))
|
||||
.mockResolvedValueOnce({
|
||||
data: [{ indexing_status: 'completed' }],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
// First call: rejects
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
// Advance past polling interval for retry
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2500)
|
||||
})
|
||||
|
||||
expect(result.current.isEmbeddingCompleted).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect embedding statuses', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [
|
||||
{ indexing_status: 'splitting' },
|
||||
{ indexing_status: 'parsing' },
|
||||
],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
|
||||
expect(result.current.isEmbedding).toBe(true)
|
||||
expect(result.current.isEmbeddingCompleted).toBe(false)
|
||||
})
|
||||
|
||||
it('should detect mixed statuses (some completed, some embedding)', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [
|
||||
{ indexing_status: 'completed' },
|
||||
{ indexing_status: 'indexing' },
|
||||
],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
|
||||
expect(result.current.statusList).toHaveLength(2)
|
||||
expect(result.current.isEmbedding).toBe(true)
|
||||
expect(result.current.isEmbeddingCompleted).toBe(false)
|
||||
})
|
||||
|
||||
it('should cleanup on unmount', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [{ indexing_status: 'indexing' }],
|
||||
})
|
||||
|
||||
const { unmount } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
|
||||
const callCount = mockFetchIndexingStatusBatch.mock.calls.length
|
||||
unmount()
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
})
|
||||
expect(mockFetchIndexingStatusBatch).toHaveBeenCalledTimes(callCount)
|
||||
})
|
||||
|
||||
it('should treat error and paused as completed statuses', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [
|
||||
{ indexing_status: 'error' },
|
||||
{ indexing_status: 'paused' },
|
||||
],
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
|
||||
expect(result.current.isEmbeddingCompleted).toBe(true)
|
||||
expect(result.current.isEmbedding).toBe(false)
|
||||
})
|
||||
|
||||
it('should poll at 2500ms intervals', async () => {
|
||||
mockFetchIndexingStatusBatch.mockResolvedValue({
|
||||
data: [{ indexing_status: 'indexing' }],
|
||||
})
|
||||
|
||||
renderHook(() => useIndexingStatusPolling(defaultParams))
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
})
|
||||
expect(mockFetchIndexingStatusBatch).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2500)
|
||||
})
|
||||
expect(mockFetchIndexingStatusBatch).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,140 @@
|
||||
import type { DataSourceInfo, FullDocumentDetail, IndexingStatusResponse } from '@/models/datasets'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createDocumentLookup, getFileType, getSourcePercent, isLegacyDataSourceInfo, isSourceEmbedding } from '../utils'
|
||||
|
||||
describe('isLegacyDataSourceInfo', () => {
|
||||
it('should return true when upload_file object exists', () => {
|
||||
const info = { upload_file: { id: '1', name: 'test.pdf' } } as unknown as DataSourceInfo
|
||||
expect(isLegacyDataSourceInfo(info)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when upload_file is absent', () => {
|
||||
const info = { notion_page_icon: 'icon' } as unknown as DataSourceInfo
|
||||
expect(isLegacyDataSourceInfo(info)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for null', () => {
|
||||
expect(isLegacyDataSourceInfo(null as unknown as DataSourceInfo)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when upload_file is a string', () => {
|
||||
const info = { upload_file: 'not-an-object' } as unknown as DataSourceInfo
|
||||
expect(isLegacyDataSourceInfo(info)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSourceEmbedding', () => {
|
||||
const embeddingStatuses = ['indexing', 'splitting', 'parsing', 'cleaning', 'waiting']
|
||||
const nonEmbeddingStatuses = ['completed', 'error', 'paused', 'unknown']
|
||||
|
||||
it.each(embeddingStatuses)('should return true for status "%s"', (status) => {
|
||||
expect(isSourceEmbedding({ indexing_status: status } as IndexingStatusResponse)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(nonEmbeddingStatuses)('should return false for status "%s"', (status) => {
|
||||
expect(isSourceEmbedding({ indexing_status: status } as IndexingStatusResponse)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSourcePercent', () => {
|
||||
it('should calculate correct percentage', () => {
|
||||
expect(getSourcePercent({ completed_segments: 50, total_segments: 100 } as IndexingStatusResponse)).toBe(50)
|
||||
})
|
||||
|
||||
it('should return 0 when total is 0', () => {
|
||||
expect(getSourcePercent({ completed_segments: 0, total_segments: 0 } as IndexingStatusResponse)).toBe(0)
|
||||
})
|
||||
|
||||
it('should cap at 100', () => {
|
||||
expect(getSourcePercent({ completed_segments: 150, total_segments: 100 } as IndexingStatusResponse)).toBe(100)
|
||||
})
|
||||
|
||||
it('should round to nearest integer', () => {
|
||||
expect(getSourcePercent({ completed_segments: 1, total_segments: 3 } as IndexingStatusResponse)).toBe(33)
|
||||
})
|
||||
|
||||
it('should handle undefined segments as 0', () => {
|
||||
expect(getSourcePercent({} as IndexingStatusResponse)).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFileType', () => {
|
||||
it('should extract extension from filename', () => {
|
||||
expect(getFileType('document.pdf')).toBe('pdf')
|
||||
})
|
||||
|
||||
it('should return last extension for multi-dot names', () => {
|
||||
expect(getFileType('archive.tar.gz')).toBe('gz')
|
||||
})
|
||||
|
||||
it('should default to "txt" for undefined', () => {
|
||||
expect(getFileType(undefined)).toBe('txt')
|
||||
})
|
||||
|
||||
it('should default to "txt" for empty string', () => {
|
||||
expect(getFileType('')).toBe('txt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('createDocumentLookup', () => {
|
||||
const documents = [
|
||||
{
|
||||
id: 'doc-1',
|
||||
name: 'test.pdf',
|
||||
data_source_type: 'upload_file',
|
||||
data_source_info: {
|
||||
upload_file: { id: 'f1', name: 'test.pdf' },
|
||||
notion_page_icon: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'doc-2',
|
||||
name: 'notion-page',
|
||||
data_source_type: 'notion_import',
|
||||
data_source_info: {
|
||||
upload_file: { id: 'f2', name: '' },
|
||||
notion_page_icon: 'https://icon.url',
|
||||
},
|
||||
},
|
||||
] as unknown as FullDocumentDetail[]
|
||||
|
||||
it('should get document by id', () => {
|
||||
const lookup = createDocumentLookup(documents)
|
||||
expect(lookup.getDocument('doc-1')).toBe(documents[0])
|
||||
})
|
||||
|
||||
it('should return undefined for non-existent id', () => {
|
||||
const lookup = createDocumentLookup(documents)
|
||||
expect(lookup.getDocument('non-existent')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should get name by id', () => {
|
||||
const lookup = createDocumentLookup(documents)
|
||||
expect(lookup.getName('doc-1')).toBe('test.pdf')
|
||||
})
|
||||
|
||||
it('should get source type by id', () => {
|
||||
const lookup = createDocumentLookup(documents)
|
||||
expect(lookup.getSourceType('doc-1')).toBe('upload_file')
|
||||
})
|
||||
|
||||
it('should get notion icon for legacy data source', () => {
|
||||
const lookup = createDocumentLookup(documents)
|
||||
expect(lookup.getNotionIcon('doc-2')).toBe('https://icon.url')
|
||||
})
|
||||
|
||||
it('should return undefined notion icon for non-legacy info', () => {
|
||||
const docs = [{
|
||||
id: 'doc-3',
|
||||
data_source_info: { some_other: 'field' },
|
||||
}] as unknown as FullDocumentDetail[]
|
||||
const lookup = createDocumentLookup(docs)
|
||||
expect(lookup.getNotionIcon('doc-3')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle empty documents list', () => {
|
||||
const lookup = createDocumentLookup([])
|
||||
expect(lookup.getDocument('any')).toBeUndefined()
|
||||
expect(lookup.getName('any')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createEmptyDataset } from '@/service/datasets'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import EmptyDatasetCreationModal from './index'
|
||||
import EmptyDatasetCreationModal from '../index'
|
||||
|
||||
// Mock Next.js router
|
||||
const mockPush = vi.fn()
|
||||
@ -54,15 +54,11 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
} as ReturnType<typeof createEmptyDataset> extends Promise<infer T> ? T : never)
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests - Verify component renders correctly
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing when show is true', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert - Check modal title is rendered
|
||||
@ -70,13 +66,10 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should render modal with correct elements', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.tip')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.input')).toBeInTheDocument()
|
||||
@ -86,22 +79,17 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should render input with empty value initially', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
expect(input.value).toBe('')
|
||||
})
|
||||
|
||||
it('should not render modal content when show is false', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ show: false })
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert - Modal should not be visible (check for absence of title)
|
||||
@ -109,29 +97,22 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing - Verify all prop variations work correctly
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('show prop', () => {
|
||||
it('should show modal when show is true', () => {
|
||||
// Arrange & Act
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide modal when show is false', () => {
|
||||
// Arrange & Act
|
||||
render(<EmptyDatasetCreationModal show={false} onHide={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle visibility when show prop changes', () => {
|
||||
// Arrange
|
||||
const onHide = vi.fn()
|
||||
const { rerender } = render(<EmptyDatasetCreationModal show={false} onHide={onHide} />)
|
||||
|
||||
@ -146,20 +127,16 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
|
||||
describe('onHide prop', () => {
|
||||
it('should call onHide when cancel button is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
|
||||
// Act
|
||||
const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onHide when close icon is clicked', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
|
||||
@ -172,31 +149,24 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
// Assert
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// State Management - Test input state updates
|
||||
// ==========================================
|
||||
describe('State Management', () => {
|
||||
it('should update input value when user types', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'My Dataset' } })
|
||||
|
||||
// Assert
|
||||
expect(input.value).toBe('My Dataset')
|
||||
})
|
||||
|
||||
it('should persist input value when modal is hidden and shown again via rerender', () => {
|
||||
// Arrange
|
||||
const onHide = vi.fn()
|
||||
const { rerender } = render(<EmptyDatasetCreationModal show={true} onHide={onHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
@ -215,12 +185,10 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should handle consecutive input changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
|
||||
// Act & Assert
|
||||
fireEvent.change(input, { target: { value: 'A' } })
|
||||
expect(input.value).toBe('A')
|
||||
|
||||
@ -232,29 +200,23 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions - Test event handlers
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should submit form when confirm button is clicked with valid input', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Valid Dataset Name' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Valid Dataset Name' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error notification when input is empty', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
@ -262,7 +224,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
// Act - Click confirm without entering a name
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -273,7 +234,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should show error notification when input exceeds 40 characters', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
@ -284,7 +244,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
fireEvent.change(input, { target: { value: longName } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -295,7 +254,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should allow exactly 40 characters', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
@ -306,94 +264,76 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
fireEvent.change(input, { target: { value: exactLengthName } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: exactLengthName })
|
||||
})
|
||||
})
|
||||
|
||||
it('should close modal on cancel button click', () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton')
|
||||
|
||||
// Act
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// API Calls - Test API interactions
|
||||
// ==========================================
|
||||
describe('API Calls', () => {
|
||||
it('should call createEmptyDataset with correct parameters', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'New Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'New Dataset' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should call invalidDatasetList after successful creation', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockInvalidDatasetList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onHide after successful creation', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockOnHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error notification on API failure', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -403,14 +343,12 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should not call onHide on API failure', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
@ -423,18 +361,15 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should not invalidate dataset list on API failure', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalled()
|
||||
})
|
||||
@ -442,12 +377,9 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Router Navigation - Test Next.js router
|
||||
// ==========================================
|
||||
describe('Router Navigation', () => {
|
||||
it('should navigate to dataset documents page after successful creation', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockResolvedValue({
|
||||
id: 'test-dataset-456',
|
||||
name: 'Test',
|
||||
@ -457,18 +389,15 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-456/documents')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not navigate on validation error', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
@ -476,7 +405,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
// Act - Click confirm with empty input
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalled()
|
||||
})
|
||||
@ -484,18 +412,15 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should not navigate on API error', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalled()
|
||||
})
|
||||
@ -503,12 +428,9 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases - Test boundary conditions and error handling
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle whitespace-only input as valid (component behavior)', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
@ -525,41 +447,34 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should handle special characters in input', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test @#$% Dataset!' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Test @#$% Dataset!' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle Unicode characters in input', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: '数据集测试 🚀' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: '数据集测试 🚀' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle input at exactly 40 character boundary', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
@ -570,14 +485,12 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
fireEvent.change(input, { target: { value: name40Chars } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: name40Chars })
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject input at 41 character boundary', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
@ -588,7 +501,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
fireEvent.change(input, { target: { value: name41Chars } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -599,7 +511,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should handle rapid consecutive submits', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
@ -618,13 +529,11 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should handle input with leading/trailing spaces', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: ' Dataset Name ' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
@ -635,13 +544,11 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should handle newline characters in input (browser strips newlines)', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Line1\nLine2' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
@ -652,20 +559,15 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Validation Tests - Test input validation
|
||||
// ==========================================
|
||||
describe('Validation', () => {
|
||||
it('should not submit when input is empty string', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
@ -675,13 +577,11 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should validate length before calling API', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'A'.repeat(50) } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
@ -696,7 +596,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should validate empty string before length check', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
@ -714,12 +613,9 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Integration Tests - Test complete flows
|
||||
// ==========================================
|
||||
describe('Integration', () => {
|
||||
it('should complete full successful creation flow', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
mockCreateEmptyDataset.mockResolvedValue({
|
||||
id: 'new-id-789',
|
||||
@ -729,7 +625,6 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Complete Flow Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
@ -747,14 +642,12 @@ describe('EmptyDatasetCreationModal', () => {
|
||||
})
|
||||
|
||||
it('should handle error flow correctly', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = vi.fn()
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('Server Error'))
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Error Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
@ -2,7 +2,7 @@ import type { MockedFunction } from 'vitest'
|
||||
import type { CustomFile as File } from '@/models/datasets'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fetchFilePreview } from '@/service/common'
|
||||
import FilePreview from './index'
|
||||
import FilePreview from '../index'
|
||||
|
||||
// Mock the fetchFilePreview service
|
||||
vi.mock('@/service/common', () => ({
|
||||
@ -48,9 +48,7 @@ const findLoadingSpinner = (container: HTMLElement) => {
|
||||
return container.querySelector('.spin-animation')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FilePreview Component Tests
|
||||
// ============================================================================
|
||||
describe('FilePreview', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -58,33 +56,25 @@ describe('FilePreview', () => {
|
||||
mockFetchFilePreview.mockResolvedValue({ content: 'Preview content here' })
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests - Verify component renders properly
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
// Arrange & Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('datasetCreation.stepOne.filePreview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render file preview header', async () => {
|
||||
// Arrange & Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.filePreview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button with XMarkIcon', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
const xMarkIcon = closeButton?.querySelector('svg')
|
||||
@ -92,42 +82,32 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should render file name without extension', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'document.pdf' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('document')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render file extension', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ extension: 'pdf' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply correct CSS classes to container', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('h-full')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Loading State Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Loading State', () => {
|
||||
it('should show loading indicator initially', async () => {
|
||||
// Arrange - Delay API response to keep loading state
|
||||
@ -135,7 +115,6 @@ describe('FilePreview', () => {
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'test' }), 100)),
|
||||
)
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Loading should be visible initially (using spin-animation class)
|
||||
@ -144,13 +123,10 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should hide loading indicator after content loads', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockResolvedValue({ content: 'Loaded content' })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Loaded content')).toBeInTheDocument()
|
||||
})
|
||||
@ -160,7 +136,6 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should show loading when file changes', async () => {
|
||||
// Arrange
|
||||
const file1 = createMockFile({ id: 'file-1', name: 'file1.txt' })
|
||||
const file2 = createMockFile({ id: 'file-2', name: 'file2.txt' })
|
||||
|
||||
@ -207,48 +182,36 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// API Call Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('API Calls', () => {
|
||||
it('should call fetchFilePreview with correct fileID', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ id: 'test-file-id' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'test-file-id' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call fetchFilePreview when file is undefined', async () => {
|
||||
// Arrange & Act
|
||||
renderFilePreview({ file: undefined })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchFilePreview).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call fetchFilePreview when file has no id', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ id: undefined })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchFilePreview).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call fetchFilePreview again when file changes', async () => {
|
||||
// Arrange
|
||||
const file1 = createMockFile({ id: 'file-1' })
|
||||
const file2 = createMockFile({ id: 'file-2' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<FilePreview file={file1} hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -259,7 +222,6 @@ describe('FilePreview', () => {
|
||||
|
||||
rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'file-2' })
|
||||
expect(mockFetchFilePreview).toHaveBeenCalledTimes(2)
|
||||
@ -267,23 +229,18 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle API success and display content', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockResolvedValue({ content: 'File preview content from API' })
|
||||
|
||||
// Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('File preview content from API')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle API error gracefully', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Component should not crash, loading may persist
|
||||
@ -295,10 +252,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle empty content response', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockResolvedValue({ content: '' })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Should still render without loading
|
||||
@ -309,29 +264,21 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// User Interactions Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
const { container } = renderFilePreview({ hidePreview })
|
||||
|
||||
// Act
|
||||
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
// Assert
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call hidePreview with event object when clicked', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
const { container } = renderFilePreview({ hidePreview })
|
||||
|
||||
// Act
|
||||
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
@ -341,52 +288,40 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle multiple clicks on close button', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
const { container } = renderFilePreview({ hidePreview })
|
||||
|
||||
// Act
|
||||
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(closeButton)
|
||||
fireEvent.click(closeButton)
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
// Assert
|
||||
expect(hidePreview).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// State Management Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('State Management', () => {
|
||||
it('should initialize with loading state true', async () => {
|
||||
// Arrange - Keep loading indefinitely (never resolves)
|
||||
mockFetchFilePreview.mockImplementation(() => new Promise(() => { /* intentionally empty */ }))
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
const loadingElement = findLoadingSpinner(container)
|
||||
expect(loadingElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update previewContent state after successful fetch', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockResolvedValue({ content: 'New preview content' })
|
||||
|
||||
// Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('New preview content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should reset loading to true when file changes', async () => {
|
||||
// Arrange
|
||||
const file1 = createMockFile({ id: 'file-1' })
|
||||
const file2 = createMockFile({ id: 'file-2' })
|
||||
|
||||
@ -394,7 +329,6 @@ describe('FilePreview', () => {
|
||||
.mockResolvedValueOnce({ content: 'Content 1' })
|
||||
.mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))
|
||||
|
||||
// Act
|
||||
const { rerender, container } = render(
|
||||
<FilePreview file={file1} hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -414,7 +348,6 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should preserve content until new content loads', async () => {
|
||||
// Arrange
|
||||
const file1 = createMockFile({ id: 'file-1' })
|
||||
const file2 = createMockFile({ id: 'file-2' })
|
||||
|
||||
@ -424,7 +357,6 @@ describe('FilePreview', () => {
|
||||
.mockResolvedValueOnce({ content: 'Content 1' })
|
||||
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<FilePreview file={file1} hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -448,25 +380,18 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('file prop', () => {
|
||||
it('should render correctly with file prop', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'my-document.pdf', extension: 'pdf' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('my-document')).toBeInTheDocument()
|
||||
expect(screen.getByText('.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render correctly without file prop', async () => {
|
||||
// Arrange & Act
|
||||
renderFilePreview({ file: undefined })
|
||||
|
||||
// Assert - Header should still render
|
||||
@ -474,10 +399,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle file with multiple dots in name', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'my.document.v2.pdf' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert - Should join all parts except last with comma
|
||||
@ -485,10 +408,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle file with no extension in name', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'README' })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview({ file })
|
||||
|
||||
// Assert - getFileName returns empty for single segment, but component still renders
|
||||
@ -500,10 +421,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle file with empty name', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: '' })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview({ file })
|
||||
|
||||
// Assert - Should not crash
|
||||
@ -513,10 +432,8 @@ describe('FilePreview', () => {
|
||||
|
||||
describe('hidePreview prop', () => {
|
||||
it('should accept hidePreview callback', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
|
||||
// Act
|
||||
renderFilePreview({ hidePreview })
|
||||
|
||||
// Assert - No errors thrown
|
||||
@ -525,15 +442,10 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle file with undefined id', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ id: undefined })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview({ file })
|
||||
|
||||
// Assert - Should not call API, remain in loading state
|
||||
@ -542,10 +454,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle file with empty string id', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ id: '' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert - Empty string is falsy, should not call API
|
||||
@ -553,48 +463,37 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle very long file names', async () => {
|
||||
// Arrange
|
||||
const longName = `${'a'.repeat(200)}.pdf`
|
||||
const file = createMockFile({ name: longName })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('a'.repeat(200))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle file with special characters in name', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'file-with_special@#$%.txt' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('file-with_special@#$%')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long preview content', async () => {
|
||||
// Arrange
|
||||
const longContent = 'x'.repeat(10000)
|
||||
mockFetchFilePreview.mockResolvedValue({ content: longContent })
|
||||
|
||||
// Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(longContent)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle preview content with special characters safely', async () => {
|
||||
// Arrange
|
||||
const specialContent = '<script>alert("xss")</script>\n\t& < > "'
|
||||
mockFetchFilePreview.mockResolvedValue({ content: specialContent })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Should render as text, not execute scripts
|
||||
@ -607,25 +506,20 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle preview content with unicode', async () => {
|
||||
// Arrange
|
||||
const unicodeContent = '中文内容 🚀 émojis & spëcîal çhàrs'
|
||||
mockFetchFilePreview.mockResolvedValue({ content: unicodeContent })
|
||||
|
||||
// Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle preview content with newlines', async () => {
|
||||
// Arrange
|
||||
const multilineContent = 'Line 1\nLine 2\nLine 3'
|
||||
mockFetchFilePreview.mockResolvedValue({ content: multilineContent })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Content should be in the DOM
|
||||
@ -639,10 +533,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle null content from API', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockResolvedValue({ content: null as unknown as string })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Should not crash
|
||||
@ -652,16 +544,12 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Side Effects and Cleanup Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Side Effects and Cleanup', () => {
|
||||
it('should trigger effect when file prop changes', async () => {
|
||||
// Arrange
|
||||
const file1 = createMockFile({ id: 'file-1' })
|
||||
const file2 = createMockFile({ id: 'file-2' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<FilePreview file={file1} hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -672,19 +560,16 @@ describe('FilePreview', () => {
|
||||
|
||||
rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockFetchFilePreview).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger effect when hidePreview changes', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile()
|
||||
const hidePreview1 = vi.fn()
|
||||
const hidePreview2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<FilePreview file={file} hidePreview={hidePreview1} />,
|
||||
)
|
||||
@ -703,11 +588,9 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle rapid file changes', async () => {
|
||||
// Arrange
|
||||
const files = Array.from({ length: 5 }, (_, i) =>
|
||||
createMockFile({ id: `file-${i}` }))
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<FilePreview file={files[0]} hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -723,12 +606,10 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle unmount during loading', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
|
||||
)
|
||||
|
||||
// Act
|
||||
const { unmount } = renderFilePreview()
|
||||
|
||||
// Unmount before API resolves
|
||||
@ -739,10 +620,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle file changing from defined to undefined', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile()
|
||||
|
||||
// Act
|
||||
const { rerender, container } = render(
|
||||
<FilePreview file={file} hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -759,26 +638,19 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// getFileName Helper Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('getFileName Helper', () => {
|
||||
it('should extract name without extension for simple filename', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'document.pdf' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('document')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle filename with multiple dots', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'file.name.with.dots.txt' })
|
||||
|
||||
// Act
|
||||
renderFilePreview({ file })
|
||||
|
||||
// Assert - Should join all parts except last with comma
|
||||
@ -786,10 +658,8 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should return empty for filename without dot', async () => {
|
||||
// Arrange
|
||||
const file = createMockFile({ name: 'nodotfile' })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview({ file })
|
||||
|
||||
// Assert - slice(0, -1) on single element array returns empty
|
||||
@ -799,7 +669,6 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should return empty string when file is undefined', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderFilePreview({ file: undefined })
|
||||
|
||||
// Assert - File name area should have empty first span
|
||||
@ -808,38 +677,27 @@ describe('FilePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Accessibility Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have clickable close button with visual indicator', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
expect(closeButton).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('should have proper heading structure', async () => {
|
||||
// Arrange & Act
|
||||
renderFilePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.filePreview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Error Handling Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Error Handling', () => {
|
||||
it('should not crash on API network error', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockRejectedValue(new Error('Network Error'))
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert - Component should still render
|
||||
@ -849,26 +707,20 @@ describe('FilePreview', () => {
|
||||
})
|
||||
|
||||
it('should not crash on API timeout', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockRejectedValue(new Error('Timeout'))
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not crash on malformed API response', async () => {
|
||||
// Arrange
|
||||
mockFetchFilePreview.mockResolvedValue({} as { content: string })
|
||||
|
||||
// Act
|
||||
const { container } = renderFilePreview()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
PROGRESS_COMPLETE,
|
||||
PROGRESS_ERROR,
|
||||
PROGRESS_NOT_STARTED,
|
||||
} from '../constants'
|
||||
|
||||
describe('file-uploader constants', () => {
|
||||
// Verify progress sentinel values
|
||||
describe('Progress Sentinels', () => {
|
||||
it('should define PROGRESS_NOT_STARTED as -1', () => {
|
||||
expect(PROGRESS_NOT_STARTED).toBe(-1)
|
||||
})
|
||||
|
||||
it('should define PROGRESS_ERROR as -2', () => {
|
||||
expect(PROGRESS_ERROR).toBe(-2)
|
||||
})
|
||||
|
||||
it('should define PROGRESS_COMPLETE as 100', () => {
|
||||
expect(PROGRESS_COMPLETE).toBe(100)
|
||||
})
|
||||
|
||||
it('should have distinct values for all sentinels', () => {
|
||||
const values = [PROGRESS_NOT_STARTED, PROGRESS_ERROR, PROGRESS_COMPLETE]
|
||||
expect(new Set(values).size).toBe(values.length)
|
||||
})
|
||||
|
||||
it('should have negative values for non-progress states', () => {
|
||||
expect(PROGRESS_NOT_STARTED).toBeLessThan(0)
|
||||
expect(PROGRESS_ERROR).toBeLessThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,26 +1,9 @@
|
||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PROGRESS_NOT_STARTED } from './constants'
|
||||
import FileUploader from './index'
|
||||
import { PROGRESS_NOT_STARTED } from '../constants'
|
||||
import FileUploader from '../index'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'stepOne.uploader.title': 'Upload Files',
|
||||
'stepOne.uploader.button': 'Drag and drop files, or',
|
||||
'stepOne.uploader.buttonSingleFile': 'Drag and drop file, or',
|
||||
'stepOne.uploader.browse': 'Browse',
|
||||
'stepOne.uploader.tip': 'Supports various file types',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock ToastContext
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('use-context-selector', async () => {
|
||||
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
|
||||
@ -118,22 +101,22 @@ describe('FileUploader', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render the component', () => {
|
||||
render(<FileUploader {...defaultProps} />)
|
||||
expect(screen.getByText('Upload Files')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.uploader.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render dropzone when no files', () => {
|
||||
render(<FileUploader {...defaultProps} />)
|
||||
expect(screen.getByText(/Drag and drop files/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.button/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render browse button', () => {
|
||||
render(<FileUploader {...defaultProps} />)
|
||||
expect(screen.getByText('Browse')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.uploader.browse')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom title className', () => {
|
||||
render(<FileUploader {...defaultProps} titleClassName="custom-class" />)
|
||||
const title = screen.getByText('Upload Files')
|
||||
const title = screen.getByText('datasetCreation.stepOne.uploader.title')
|
||||
expect(title).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
@ -162,19 +145,19 @@ describe('FileUploader', () => {
|
||||
describe('batch upload mode', () => {
|
||||
it('should show dropzone with batch upload enabled', () => {
|
||||
render(<FileUploader {...defaultProps} supportBatchUpload={true} />)
|
||||
expect(screen.getByText(/Drag and drop files/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.button/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show single file text when batch upload disabled', () => {
|
||||
render(<FileUploader {...defaultProps} supportBatchUpload={false} />)
|
||||
expect(screen.getByText(/Drag and drop file/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.buttonSingleFile/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide dropzone when not batch upload and has files', () => {
|
||||
const fileList = [createMockFileItem()]
|
||||
render(<FileUploader {...defaultProps} supportBatchUpload={false} fileList={fileList} />)
|
||||
|
||||
expect(screen.queryByText(/Drag and drop/i)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.button/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -217,7 +200,7 @@ describe('FileUploader', () => {
|
||||
render(<FileUploader {...defaultProps} />)
|
||||
|
||||
// The browse label should trigger file input click
|
||||
const browseLabel = screen.getByText('Browse')
|
||||
const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse')
|
||||
expect(browseLabel).toHaveClass('cursor-pointer')
|
||||
})
|
||||
})
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FileListItemProps } from './file-list-item'
|
||||
import type { FileListItemProps } from '../file-list-item'
|
||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../constants'
|
||||
import FileListItem from './file-list-item'
|
||||
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
|
||||
import FileListItem from '../file-list-item'
|
||||
|
||||
// Mock theme hook - can be changed per test
|
||||
let mockTheme = 'light'
|
||||
@ -1,33 +1,12 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { UploadDropzoneProps } from './upload-dropzone'
|
||||
import type { UploadDropzoneProps } from '../upload-dropzone'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import UploadDropzone from './upload-dropzone'
|
||||
import UploadDropzone from '../upload-dropzone'
|
||||
|
||||
// Helper to create mock ref objects for testing
|
||||
const createMockRef = <T,>(value: T | null = null): RefObject<T | null> => ({ current: value })
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
const translations: Record<string, string> = {
|
||||
'stepOne.uploader.button': 'Drag and drop files, or',
|
||||
'stepOne.uploader.buttonSingleFile': 'Drag and drop file, or',
|
||||
'stepOne.uploader.browse': 'Browse',
|
||||
'stepOne.uploader.tip': 'Supports {{supportTypes}}, Max {{size}}MB each, up to {{batchCount}} files at a time, {{totalCount}} files total',
|
||||
}
|
||||
let result = translations[key] || key
|
||||
if (options && typeof options === 'object') {
|
||||
Object.entries(options).forEach(([k, v]) => {
|
||||
result = result.replace(`{{${k}}}`, String(v))
|
||||
})
|
||||
}
|
||||
return result
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('UploadDropzone', () => {
|
||||
const defaultProps: UploadDropzoneProps = {
|
||||
dropRef: createMockRef<HTMLDivElement>() as RefObject<HTMLDivElement | null>,
|
||||
@ -73,17 +52,17 @@ describe('UploadDropzone', () => {
|
||||
|
||||
it('should render browse label when extensions are allowed', () => {
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
expect(screen.getByText('Browse')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.uploader.browse')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render browse label when no extensions allowed', () => {
|
||||
render(<UploadDropzone {...defaultProps} acceptTypes={[]} />)
|
||||
expect(screen.queryByText('Browse')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('datasetCreation.stepOne.uploader.browse')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render file size and count limits', () => {
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
const tipText = screen.getByText(/Supports.*Max.*15MB/i)
|
||||
const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip/)
|
||||
expect(tipText).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -111,12 +90,12 @@ describe('UploadDropzone', () => {
|
||||
describe('text content', () => {
|
||||
it('should show batch upload text when supportBatchUpload is true', () => {
|
||||
render(<UploadDropzone {...defaultProps} supportBatchUpload={true} />)
|
||||
expect(screen.getByText(/Drag and drop files/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.button/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show single file text when supportBatchUpload is false', () => {
|
||||
render(<UploadDropzone {...defaultProps} supportBatchUpload={false} />)
|
||||
expect(screen.getByText(/Drag and drop file/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.buttonSingleFile/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -146,7 +125,7 @@ describe('UploadDropzone', () => {
|
||||
const onSelectFile = vi.fn()
|
||||
render(<UploadDropzone {...defaultProps} onSelectFile={onSelectFile} />)
|
||||
|
||||
const browseLabel = screen.getByText('Browse')
|
||||
const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse')
|
||||
fireEvent.click(browseLabel)
|
||||
|
||||
expect(onSelectFile).toHaveBeenCalledTimes(1)
|
||||
@ -195,7 +174,7 @@ describe('UploadDropzone', () => {
|
||||
|
||||
it('should have cursor-pointer on browse label', () => {
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
const browseLabel = screen.getByText('Browse')
|
||||
const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse')
|
||||
expect(browseLabel).toHaveClass('cursor-pointer')
|
||||
})
|
||||
})
|
||||
@ -4,15 +4,14 @@ import { act, render, renderHook, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
|
||||
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../constants'
|
||||
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
|
||||
// Import after mocks
|
||||
import { useFileUpload } from './use-file-upload'
|
||||
import { useFileUpload } from '../use-file-upload'
|
||||
|
||||
// Mock notify function
|
||||
const mockNotify = vi.fn()
|
||||
const mockClose = vi.fn()
|
||||
|
||||
// Mock ToastContext
|
||||
vi.mock('use-context-selector', async () => {
|
||||
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
|
||||
return {
|
||||
@ -44,12 +43,6 @@ vi.mock('@/service/use-common', () => ({
|
||||
}))
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock locale
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => 'en-US',
|
||||
@ -59,7 +52,6 @@ vi.mock('@/i18n-config/language', () => ({
|
||||
LanguagesSupported: ['en-US', 'zh-Hans'],
|
||||
}))
|
||||
|
||||
// Mock config
|
||||
vi.mock('@/config', () => ({
|
||||
IS_CE_EDITION: false,
|
||||
}))
|
||||
@ -2,7 +2,7 @@ import type { MockedFunction } from 'vitest'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fetchNotionPagePreview } from '@/service/datasets'
|
||||
import NotionPagePreview from './index'
|
||||
import NotionPagePreview from '../index'
|
||||
|
||||
// Mock the fetchNotionPagePreview service
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
@ -85,13 +85,10 @@ const findLoadingSpinner = (container: HTMLElement) => {
|
||||
return container.querySelector('.spin-animation')
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NotionPagePreview Component Tests
|
||||
// ============================================================================
|
||||
// Note: Branch coverage is ~88% because line 29 (`if (!currentPage) return`)
|
||||
// is defensive code that cannot be reached - getPreviewContent is only called
|
||||
// from useEffect when currentPage is truthy.
|
||||
// ============================================================================
|
||||
describe('NotionPagePreview', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -106,31 +103,23 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests - Verify component renders properly
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
// Arrange & Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render page preview header', async () => {
|
||||
// Arrange & Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button with XMarkIcon', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
const xMarkIcon = closeButton?.querySelector('svg')
|
||||
@ -138,30 +127,23 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should render page name', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_name: 'My Notion Page' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('My Notion Page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply correct CSS classes to container', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('h-full')
|
||||
})
|
||||
|
||||
it('should render NotionIcon component', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage()
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - NotionIcon should be rendered (either as img or div or svg)
|
||||
@ -170,15 +152,11 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// NotionIcon Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('NotionIcon Rendering', () => {
|
||||
it('should render default icon when page_icon is null', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_icon: null })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - Should render RiFileTextLine icon (svg)
|
||||
@ -187,33 +165,25 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should render emoji icon when page_icon has emoji type', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPageWithEmojiIcon('📝')
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('📝')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render image icon when page_icon has url type', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPageWithUrlIcon('https://example.com/icon.png')
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
const img = container.querySelector('img[alt="page icon"]')
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/icon.png')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Loading State Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Loading State', () => {
|
||||
it('should show loading indicator initially', async () => {
|
||||
// Arrange - Delay API response to keep loading state
|
||||
@ -230,13 +200,10 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should hide loading indicator after content loads', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Loaded content' })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Loaded content')).toBeInTheDocument()
|
||||
// Loading should be gone
|
||||
const loadingElement = findLoadingSpinner(container)
|
||||
@ -244,7 +211,6 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should show loading when currentPage changes', async () => {
|
||||
// Arrange
|
||||
const page1 = createMockNotionPage({ page_id: 'page-1', page_name: 'Page 1' })
|
||||
const page2 = createMockNotionPage({ page_id: 'page-2', page_name: 'Page 2' })
|
||||
|
||||
@ -291,24 +257,19 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// API Call Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('API Calls', () => {
|
||||
it('should call fetchNotionPagePreview with correct parameters', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({
|
||||
page_id: 'test-page-id',
|
||||
type: 'database',
|
||||
})
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({
|
||||
currentPage: page,
|
||||
notionCredentialId: 'test-credential-id',
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
|
||||
pageID: 'test-page-id',
|
||||
pageType: 'database',
|
||||
@ -317,19 +278,15 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should not call fetchNotionPagePreview when currentPage is undefined', async () => {
|
||||
// Arrange & Act
|
||||
await renderNotionPagePreview({ currentPage: undefined }, false)
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call fetchNotionPagePreview again when currentPage changes', async () => {
|
||||
// Arrange
|
||||
const page1 = createMockNotionPage({ page_id: 'page-1' })
|
||||
const page2 = createMockNotionPage({ page_id: 'page-2' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -346,7 +303,6 @@ describe('NotionPagePreview', () => {
|
||||
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
|
||||
pageID: 'page-2',
|
||||
@ -358,21 +314,16 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle API success and display content', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Notion page preview content from API' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Notion page preview content from API')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle API error gracefully', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert - Component should not crash
|
||||
@ -384,10 +335,8 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle empty content response', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: '' })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert - Should still render without loading
|
||||
@ -396,42 +345,30 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// User Interactions Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
const { container } = await renderNotionPagePreview({ hidePreview })
|
||||
|
||||
// Act
|
||||
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
// Assert
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle multiple clicks on close button', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
const { container } = await renderNotionPagePreview({ hidePreview })
|
||||
|
||||
// Act
|
||||
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(closeButton)
|
||||
fireEvent.click(closeButton)
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
// Assert
|
||||
expect(hidePreview).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// State Management Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('State Management', () => {
|
||||
it('should initialize with loading state true', async () => {
|
||||
// Arrange - Keep loading indefinitely (never resolves)
|
||||
@ -440,24 +377,19 @@ describe('NotionPagePreview', () => {
|
||||
// Act - Don't wait for content
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert
|
||||
const loadingElement = findLoadingSpinner(container)
|
||||
expect(loadingElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update previewContent state after successful fetch', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: 'New preview content' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('New preview content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should reset loading to true when currentPage changes', async () => {
|
||||
// Arrange
|
||||
const page1 = createMockNotionPage({ page_id: 'page-1' })
|
||||
const page2 = createMockNotionPage({ page_id: 'page-2' })
|
||||
|
||||
@ -465,7 +397,6 @@ describe('NotionPagePreview', () => {
|
||||
.mockResolvedValueOnce({ content: 'Content 1' })
|
||||
.mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))
|
||||
|
||||
// Act
|
||||
const { rerender, container } = render(
|
||||
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -487,7 +418,6 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should replace old content with new content when page changes', async () => {
|
||||
// Arrange
|
||||
const page1 = createMockNotionPage({ page_id: 'page-1' })
|
||||
const page2 = createMockNotionPage({ page_id: 'page-2' })
|
||||
|
||||
@ -497,7 +427,6 @@ describe('NotionPagePreview', () => {
|
||||
.mockResolvedValueOnce({ content: 'Content 1' })
|
||||
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -523,24 +452,17 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('currentPage prop', () => {
|
||||
it('should render correctly with currentPage prop', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_name: 'My Test Page' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('My Test Page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render correctly without currentPage prop (undefined)', async () => {
|
||||
// Arrange & Act
|
||||
await renderNotionPagePreview({ currentPage: undefined }, false)
|
||||
|
||||
// Assert - Header should still render
|
||||
@ -548,10 +470,8 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle page with empty name', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_name: '' })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - Should not crash
|
||||
@ -559,52 +479,40 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle page with very long name', async () => {
|
||||
// Arrange
|
||||
const longName = 'a'.repeat(200)
|
||||
const page = createMockNotionPage({ page_name: longName })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longName)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle page with special characters in name', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_name: 'Page with <special> & "chars"' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Page with <special> & "chars"')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle page with unicode characters in name', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_name: '中文页面名称 🚀 日本語' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('中文页面名称 🚀 日本語')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('notionCredentialId prop', () => {
|
||||
it('should pass notionCredentialId to API call', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage()
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({
|
||||
currentPage: page,
|
||||
notionCredentialId: 'my-credential-id',
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ credentialID: 'my-credential-id' }),
|
||||
)
|
||||
@ -613,10 +521,8 @@ describe('NotionPagePreview', () => {
|
||||
|
||||
describe('hidePreview prop', () => {
|
||||
it('should accept hidePreview callback', async () => {
|
||||
// Arrange
|
||||
const hidePreview = vi.fn()
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ hidePreview })
|
||||
|
||||
// Assert - No errors thrown
|
||||
@ -625,15 +531,10 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle page with undefined page_id', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_id: undefined as unknown as string })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - API should still be called (with undefined pageID)
|
||||
@ -641,36 +542,28 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle page with empty string page_id', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_id: '' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pageID: '' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle very long preview content', async () => {
|
||||
// Arrange
|
||||
const longContent = 'x'.repeat(10000)
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: longContent })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle preview content with special characters safely', async () => {
|
||||
// Arrange
|
||||
const specialContent = '<script>alert("xss")</script>\n\t& < > "'
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: specialContent })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert - Should render as text, not execute scripts
|
||||
@ -680,26 +573,20 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle preview content with unicode', async () => {
|
||||
// Arrange
|
||||
const unicodeContent = '中文内容 🚀 émojis & spëcîal çhàrs'
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: unicodeContent })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle preview content with newlines', async () => {
|
||||
// Arrange
|
||||
const multilineContent = 'Line 1\nLine 2\nLine 3'
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: multilineContent })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const contentDiv = container.querySelector('[class*="fileContent"]')
|
||||
expect(contentDiv).toBeInTheDocument()
|
||||
expect(contentDiv?.textContent).toContain('Line 1')
|
||||
@ -708,10 +595,8 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle null content from API', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: null as unknown as string })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert - Should not crash
|
||||
@ -719,29 +604,22 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle different page types', async () => {
|
||||
// Arrange
|
||||
const databasePage = createMockNotionPage({ type: 'database' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: databasePage })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pageType: 'database' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Side Effects and Cleanup Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Side Effects and Cleanup', () => {
|
||||
it('should trigger effect when currentPage prop changes', async () => {
|
||||
// Arrange
|
||||
const page1 = createMockNotionPage({ page_id: 'page-1' })
|
||||
const page2 = createMockNotionPage({ page_id: 'page-2' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -754,19 +632,16 @@ describe('NotionPagePreview', () => {
|
||||
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not trigger effect when hidePreview changes', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage()
|
||||
const hidePreview1 = vi.fn()
|
||||
const hidePreview2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={hidePreview1} />,
|
||||
)
|
||||
@ -785,10 +660,8 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should not trigger effect when notionCredentialId changes', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<NotionPagePreview currentPage={page} notionCredentialId="cred-1" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -806,11 +679,9 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle rapid page changes', async () => {
|
||||
// Arrange
|
||||
const pages = Array.from({ length: 5 }, (_, i) =>
|
||||
createMockNotionPage({ page_id: `page-${i}` }))
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<NotionPagePreview currentPage={pages[0]} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -829,7 +700,6 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle unmount during loading', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
|
||||
)
|
||||
@ -845,10 +715,8 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle page changing from defined to undefined', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage()
|
||||
|
||||
// Act
|
||||
const { rerender, container } = render(
|
||||
<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
|
||||
)
|
||||
@ -867,38 +735,27 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Accessibility Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have clickable close button with visual indicator', async () => {
|
||||
// Arrange & Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
expect(closeButton).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('should have proper heading structure', async () => {
|
||||
// Arrange & Act
|
||||
await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Error Handling Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Error Handling', () => {
|
||||
it('should not crash on API network error', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockRejectedValue(new Error('Network Error'))
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert - Component should still render
|
||||
@ -908,122 +765,92 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should not crash on API timeout', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockRejectedValue(new Error('Timeout'))
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not crash on malformed API response', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({} as { content: string })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle 404 error gracefully', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockRejectedValue(new Error('404 Not Found'))
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle 500 error gracefully', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockRejectedValue(new Error('500 Internal Server Error'))
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle authorization error gracefully', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockRejectedValue(new Error('401 Unauthorized'))
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({}, false)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Page Type Variations Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Page Type Variations', () => {
|
||||
it('should handle page type', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ type: 'page' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pageType: 'page' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle database type', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ type: 'database' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pageType: 'database' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle unknown type', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ type: 'unknown_type' })
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pageType: 'unknown_type' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Icon Type Variations Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Icon Type Variations', () => {
|
||||
it('should handle page with null icon', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({ page_icon: null })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - Should render default icon
|
||||
@ -1032,31 +859,24 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle page with emoji icon object', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPageWithEmojiIcon('📄')
|
||||
|
||||
// Act
|
||||
await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('📄')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle page with url icon object', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPageWithUrlIcon('https://example.com/custom-icon.png')
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert
|
||||
const img = container.querySelector('img[alt="page icon"]')
|
||||
expect(img).toBeInTheDocument()
|
||||
expect(img).toHaveAttribute('src', 'https://example.com/custom-icon.png')
|
||||
})
|
||||
|
||||
it('should handle page with icon object having null values', async () => {
|
||||
// Arrange
|
||||
const page = createMockNotionPage({
|
||||
page_icon: {
|
||||
type: null,
|
||||
@ -1065,7 +885,6 @@ describe('NotionPagePreview', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - Should render, likely with default/fallback
|
||||
@ -1073,7 +892,6 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should handle page with icon object having empty url', async () => {
|
||||
// Arrange
|
||||
// Suppress console.error for this test as we're intentionally testing empty src edge case
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn())
|
||||
|
||||
@ -1085,7 +903,6 @@ describe('NotionPagePreview', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview({ currentPage: page })
|
||||
|
||||
// Assert - Component should not crash, may render img or fallback
|
||||
@ -1100,32 +917,24 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Content Display Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Content Display', () => {
|
||||
it('should display content in fileContent div with correct class', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Test content' })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const contentDiv = container.querySelector('[class*="fileContent"]')
|
||||
expect(contentDiv).toBeInTheDocument()
|
||||
expect(contentDiv).toHaveTextContent('Test content')
|
||||
})
|
||||
|
||||
it('should preserve whitespace in content', async () => {
|
||||
// Arrange
|
||||
const contentWithWhitespace = ' indented content\n more indent'
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: contentWithWhitespace })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const contentDiv = container.querySelector('[class*="fileContent"]')
|
||||
expect(contentDiv).toBeInTheDocument()
|
||||
// The CSS class has white-space: pre-line
|
||||
@ -1133,13 +942,10 @@ describe('NotionPagePreview', () => {
|
||||
})
|
||||
|
||||
it('should display empty string content without loading', async () => {
|
||||
// Arrange
|
||||
mockFetchNotionPagePreview.mockResolvedValue({ content: '' })
|
||||
|
||||
// Act
|
||||
const { container } = await renderNotionPagePreview()
|
||||
|
||||
// Assert
|
||||
const loadingElement = findLoadingSpinner(container)
|
||||
expect(loadingElement).not.toBeInTheDocument()
|
||||
const contentDiv = container.querySelector('[class*="fileContent"]')
|
||||
@ -0,0 +1,561 @@
|
||||
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlOptions, CrawlResultItem, DataSet, FileItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import StepOne from '../index'
|
||||
|
||||
// Mock config for website crawl features
|
||||
vi.mock('@/config', () => ({
|
||||
ENABLE_WEBSITE_FIRECRAWL: true,
|
||||
ENABLE_WEBSITE_JINAREADER: false,
|
||||
ENABLE_WEBSITE_WATERCRAWL: false,
|
||||
}))
|
||||
|
||||
// Mock dataset detail context
|
||||
let mockDatasetDetail: DataSet | undefined
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (selector: (state: { dataset: DataSet | undefined }) => DataSet | undefined) => {
|
||||
return selector({ dataset: mockDatasetDetail })
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock provider context
|
||||
let mockPlan = {
|
||||
type: Plan.professional,
|
||||
usage: { vectorSpace: 50, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
|
||||
total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
|
||||
}
|
||||
let mockEnableBilling = false
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: mockPlan,
|
||||
enableBilling: mockEnableBilling,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../file-uploader', () => ({
|
||||
default: ({ onPreview, fileList }: { onPreview: (file: File) => void, fileList: FileItem[] }) => (
|
||||
<div data-testid="file-uploader">
|
||||
<span data-testid="file-count">{fileList.length}</span>
|
||||
<button data-testid="preview-file" onClick={() => onPreview(new File(['test'], 'test.txt'))}>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../website', () => ({
|
||||
default: ({ onPreview }: { onPreview: (item: CrawlResultItem) => void }) => (
|
||||
<div data-testid="website">
|
||||
<button
|
||||
data-testid="preview-website"
|
||||
onClick={() => onPreview({ title: 'Test', markdown: '', description: '', source_url: 'https://test.com' })}
|
||||
>
|
||||
Preview Website
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../empty-dataset-creation-modal', () => ({
|
||||
default: ({ show, onHide }: { show: boolean, onHide: () => void }) => (
|
||||
show
|
||||
? (
|
||||
<div data-testid="empty-dataset-modal">
|
||||
<button data-testid="close-modal" onClick={onHide}>Close</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
// NotionConnector is a base component - imported directly without mock
|
||||
// It only depends on i18n which is globally mocked
|
||||
|
||||
vi.mock('@/app/components/base/notion-page-selector', () => ({
|
||||
NotionPageSelector: ({ onPreview }: { onPreview: (page: NotionPage) => void }) => (
|
||||
<div data-testid="notion-page-selector">
|
||||
<button
|
||||
data-testid="preview-notion"
|
||||
onClick={() => onPreview({ page_id: 'page-1', type: 'page' } as NotionPage)}
|
||||
>
|
||||
Preview Notion
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/vector-space-full', () => ({
|
||||
default: () => <div data-testid="vector-space-full">Vector Space Full</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: ({ show, onClose }: { show: boolean, onClose: () => void }) => (
|
||||
show
|
||||
? (
|
||||
<div data-testid="plan-upgrade-modal">
|
||||
<button data-testid="close-upgrade-modal" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../file-preview', () => ({
|
||||
default: ({ file, hidePreview }: { file: File, hidePreview: () => void }) => (
|
||||
<div data-testid="file-preview">
|
||||
<span>{file.name}</span>
|
||||
<button data-testid="hide-file-preview" onClick={hidePreview}>Hide</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../notion-page-preview', () => ({
|
||||
default: ({ currentPage, hidePreview }: { currentPage: NotionPage, hidePreview: () => void }) => (
|
||||
<div data-testid="notion-page-preview">
|
||||
<span>{currentPage.page_id}</span>
|
||||
<button data-testid="hide-notion-preview" onClick={hidePreview}>Hide</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// WebsitePreview is a sibling component without API dependencies - imported directly
|
||||
// It only depends on i18n which is globally mocked
|
||||
|
||||
vi.mock('../upgrade-card', () => ({
|
||||
default: () => <div data-testid="upgrade-card">Upgrade Card</div>,
|
||||
}))
|
||||
|
||||
const createMockCustomFile = (overrides: { id?: string, name?: string } = {}) => {
|
||||
const file = new File(['test content'], overrides.name ?? 'test.txt', { type: 'text/plain' })
|
||||
return Object.assign(file, {
|
||||
id: overrides.id ?? 'uploaded-id',
|
||||
extension: 'txt',
|
||||
mime_type: 'text/plain',
|
||||
created_by: 'user-1',
|
||||
created_at: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
const createMockFileItem = (overrides: Partial<FileItem> = {}): FileItem => ({
|
||||
fileID: `file-${Date.now()}`,
|
||||
file: createMockCustomFile(overrides.file as { id?: string, name?: string }),
|
||||
progress: 100,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockNotionPage = (overrides: Partial<NotionPage> = {}): NotionPage => ({
|
||||
page_id: `page-${Date.now()}`,
|
||||
type: 'page',
|
||||
...overrides,
|
||||
} as NotionPage)
|
||||
|
||||
const createMockCrawlResult = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
|
||||
title: 'Test Page',
|
||||
markdown: 'Test content',
|
||||
description: 'Test description',
|
||||
source_url: 'https://example.com',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockDataSourceAuth = (overrides: Partial<DataSourceAuth> = {}): DataSourceAuth => ({
|
||||
credential_id: 'cred-1',
|
||||
provider: 'notion_datasource',
|
||||
plugin_id: 'plugin-1',
|
||||
credentials_list: [{ id: 'cred-1', name: 'Workspace 1' }],
|
||||
...overrides,
|
||||
} as DataSourceAuth)
|
||||
|
||||
const defaultProps = {
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
dataSourceTypeDisable: false,
|
||||
onSetting: vi.fn(),
|
||||
files: [] as FileItem[],
|
||||
updateFileList: vi.fn(),
|
||||
updateFile: vi.fn(),
|
||||
notionPages: [] as NotionPage[],
|
||||
notionCredentialId: '',
|
||||
updateNotionPages: vi.fn(),
|
||||
updateNotionCredentialId: vi.fn(),
|
||||
onStepChange: vi.fn(),
|
||||
changeType: vi.fn(),
|
||||
websitePages: [] as CrawlResultItem[],
|
||||
updateWebsitePages: vi.fn(),
|
||||
onWebsiteCrawlProviderChange: vi.fn(),
|
||||
onWebsiteCrawlJobIdChange: vi.fn(),
|
||||
crawlOptions: {
|
||||
crawl_sub_pages: true,
|
||||
only_main_content: true,
|
||||
includes: '',
|
||||
excludes: '',
|
||||
limit: 10,
|
||||
max_depth: '',
|
||||
use_sitemap: true,
|
||||
} as CrawlOptions,
|
||||
onCrawlOptionsChange: vi.fn(),
|
||||
authedDataSourceList: [] as DataSourceAuth[],
|
||||
}
|
||||
|
||||
// NOTE: Child component unit tests (usePreviewState, DataSourceTypeSelector,
|
||||
// NextStepButton, PreviewPanel) have been moved to their own dedicated spec files:
|
||||
// - ./hooks/use-preview-state.spec.ts
|
||||
// - ./components/data-source-type-selector.spec.tsx
|
||||
// - ./components/next-step-button.spec.tsx
|
||||
// - ./components/preview-panel.spec.tsx
|
||||
// This file now focuses exclusively on StepOne parent component tests.
|
||||
|
||||
// StepOne Component Tests
|
||||
describe('StepOne', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDatasetDetail = undefined
|
||||
mockPlan = {
|
||||
type: Plan.professional,
|
||||
usage: { vectorSpace: 50, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
|
||||
total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 },
|
||||
}
|
||||
mockEnableBilling = false
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render DataSourceTypeSelector when not editing existing dataset', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('datasetCreation.stepOne.dataSourceType.file')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render FileUploader when dataSourceType is FILE', () => {
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.FILE} />)
|
||||
|
||||
expect(screen.getByTestId('file-uploader')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render NotionConnector when dataSourceType is NOTION and not authenticated', () => {
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} />)
|
||||
|
||||
// Assert - NotionConnector shows sync title and connect button
|
||||
expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.connect/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render NotionPageSelector when dataSourceType is NOTION and authenticated', () => {
|
||||
const authedDataSourceList = [createMockDataSourceAuth()]
|
||||
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
|
||||
|
||||
expect(screen.getByTestId('notion-page-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Website when dataSourceType is WEB', () => {
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.WEB} />)
|
||||
|
||||
expect(screen.getByTestId('website')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render empty dataset creation link when no datasetId', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render empty dataset creation link when datasetId exists', () => {
|
||||
render(<StepOne {...defaultProps} datasetId="dataset-123" />)
|
||||
|
||||
expect(screen.queryByText('datasetCreation.stepOne.emptyDatasetCreation')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props Tests
|
||||
describe('Props', () => {
|
||||
it('should pass files to FileUploader', () => {
|
||||
const files = [createMockFileItem()]
|
||||
|
||||
render(<StepOne {...defaultProps} files={files} />)
|
||||
|
||||
expect(screen.getByTestId('file-count')).toHaveTextContent('1')
|
||||
})
|
||||
|
||||
it('should call onSetting when NotionConnector connect button is clicked', () => {
|
||||
const onSetting = vi.fn()
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} onSetting={onSetting} />)
|
||||
|
||||
// Act - The NotionConnector's button calls onSetting
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.connect/i }))
|
||||
|
||||
expect(onSetting).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call changeType when data source type is changed', () => {
|
||||
const changeType = vi.fn()
|
||||
render(<StepOne {...defaultProps} changeType={changeType} />)
|
||||
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
|
||||
|
||||
expect(changeType).toHaveBeenCalledWith(DataSourceType.NOTION)
|
||||
})
|
||||
})
|
||||
|
||||
describe('State Management', () => {
|
||||
it('should open empty dataset modal when link is clicked', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation'))
|
||||
|
||||
expect(screen.getByTestId('empty-dataset-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close empty dataset modal when close is clicked', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation'))
|
||||
|
||||
fireEvent.click(screen.getByTestId('close-modal'))
|
||||
|
||||
expect(screen.queryByTestId('empty-dataset-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should correctly compute isNotionAuthed based on authedDataSourceList', () => {
|
||||
// Arrange - No auth
|
||||
const { rerender } = render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} />)
|
||||
// NotionConnector shows the sync title when not authenticated
|
||||
expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
|
||||
|
||||
// Act - Add auth
|
||||
const authedDataSourceList = [createMockDataSourceAuth()]
|
||||
rerender(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
|
||||
|
||||
expect(screen.getByTestId('notion-page-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should correctly compute fileNextDisabled when files are empty', () => {
|
||||
render(<StepOne {...defaultProps} files={[]} />)
|
||||
|
||||
// Assert - Button should be disabled
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should correctly compute fileNextDisabled when files are loaded', () => {
|
||||
const files = [createMockFileItem()]
|
||||
|
||||
render(<StepOne {...defaultProps} files={files} />)
|
||||
|
||||
// Assert - Button should be enabled
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should correctly compute fileNextDisabled when some files are not uploaded', () => {
|
||||
// Arrange - Create a file item without id (not yet uploaded)
|
||||
const file = new File(['test'], 'test.txt', { type: 'text/plain' })
|
||||
const fileItem: FileItem = {
|
||||
fileID: 'temp-id',
|
||||
file: Object.assign(file, { id: undefined, extension: 'txt', mime_type: 'text/plain' }),
|
||||
progress: 0,
|
||||
}
|
||||
|
||||
render(<StepOne {...defaultProps} files={[fileItem]} />)
|
||||
|
||||
// Assert - Button should be disabled
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Callbacks', () => {
|
||||
it('should call onStepChange when next button is clicked with valid files', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const files = [createMockFileItem()]
|
||||
render(<StepOne {...defaultProps} files={files} onStepChange={onStepChange} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show plan upgrade modal when batch upload not supported and multiple files', () => {
|
||||
mockEnableBilling = true
|
||||
mockPlan.type = Plan.sandbox
|
||||
const files = [createMockFileItem(), createMockFileItem()]
|
||||
render(<StepOne {...defaultProps} files={files} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show upgrade card when in sandbox plan with files', () => {
|
||||
mockEnableBilling = true
|
||||
mockPlan.type = Plan.sandbox
|
||||
const files = [createMockFileItem()]
|
||||
|
||||
render(<StepOne {...defaultProps} files={files} />)
|
||||
|
||||
expect(screen.getByTestId('upgrade-card')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Vector Space Full Tests
|
||||
describe('Vector Space Full', () => {
|
||||
it('should show VectorSpaceFull when vector space is full and billing is enabled', () => {
|
||||
mockEnableBilling = true
|
||||
mockPlan.usage.vectorSpace = 100
|
||||
mockPlan.total.vectorSpace = 100
|
||||
const files = [createMockFileItem()]
|
||||
|
||||
render(<StepOne {...defaultProps} files={files} />)
|
||||
|
||||
expect(screen.getByTestId('vector-space-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable next button when vector space is full', () => {
|
||||
mockEnableBilling = true
|
||||
mockPlan.usage.vectorSpace = 100
|
||||
mockPlan.total.vectorSpace = 100
|
||||
const files = [createMockFileItem()]
|
||||
|
||||
render(<StepOne {...defaultProps} files={files} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// Preview Integration Tests
|
||||
describe('Preview Integration', () => {
|
||||
it('should show file preview when file preview button is clicked', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('preview-file'))
|
||||
|
||||
expect(screen.getByTestId('file-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide file preview when hide button is clicked', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('preview-file'))
|
||||
|
||||
fireEvent.click(screen.getByTestId('hide-file-preview'))
|
||||
|
||||
expect(screen.queryByTestId('file-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show notion page preview when preview button is clicked', () => {
|
||||
const authedDataSourceList = [createMockDataSourceAuth()]
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('preview-notion'))
|
||||
|
||||
expect(screen.getByTestId('notion-page-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show website preview when preview button is clicked', () => {
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.WEB} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('preview-website'))
|
||||
|
||||
// Assert - Check for pagePreview title which is shown by WebsitePreview
|
||||
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty notionPages array', () => {
|
||||
const authedDataSourceList = [createMockDataSourceAuth()]
|
||||
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} notionPages={[]} authedDataSourceList={authedDataSourceList} />)
|
||||
|
||||
// Assert - Button should be disabled when no pages selected
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle empty websitePages array', () => {
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.WEB} websitePages={[]} />)
|
||||
|
||||
// Assert - Button should be disabled when no pages crawled
|
||||
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle empty authedDataSourceList', () => {
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={[]} />)
|
||||
|
||||
// Assert - Should show NotionConnector with connect button
|
||||
expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle authedDataSourceList without notion credentials', () => {
|
||||
const authedDataSourceList = [createMockDataSourceAuth({ credentials_list: [] })]
|
||||
|
||||
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
|
||||
|
||||
// Assert - Should show NotionConnector with connect button
|
||||
expect(screen.getByText('datasetCreation.stepOne.notionSyncTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should clear previews when switching data source types', () => {
|
||||
render(<StepOne {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('preview-file'))
|
||||
expect(screen.getByTestId('file-preview')).toBeInTheDocument()
|
||||
|
||||
// Act - Change to NOTION
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
|
||||
|
||||
// Assert - File preview should be cleared
|
||||
expect(screen.queryByTestId('file-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration', () => {
|
||||
it('should complete file upload flow', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const files = [createMockFileItem()]
|
||||
|
||||
render(<StepOne {...defaultProps} files={files} onStepChange={onStepChange} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should complete notion page selection flow', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const authedDataSourceList = [createMockDataSourceAuth()]
|
||||
const notionPages = [createMockNotionPage()]
|
||||
|
||||
render(
|
||||
<StepOne
|
||||
{...defaultProps}
|
||||
dataSourceType={DataSourceType.NOTION}
|
||||
authedDataSourceList={authedDataSourceList}
|
||||
notionPages={notionPages}
|
||||
onStepChange={onStepChange}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should complete website crawl flow', () => {
|
||||
const onStepChange = vi.fn()
|
||||
const websitePages = [createMockCrawlResult()]
|
||||
|
||||
render(
|
||||
<StepOne
|
||||
{...defaultProps}
|
||||
dataSourceType={DataSourceType.WEB}
|
||||
websitePages={websitePages}
|
||||
onStepChange={onStepChange}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
|
||||
|
||||
expect(onStepChange).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,89 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import UpgradeCard from '../upgrade-card'
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/upgrade-btn', () => ({
|
||||
default: ({ onClick, className }: { onClick?: () => void, className?: string }) => (
|
||||
<button type="button" className={className} onClick={onClick} data-testid="upgrade-btn">
|
||||
upgrade
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('UpgradeCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
// Assert - title and description i18n keys are rendered
|
||||
expect(screen.getByText(/uploadMultipleFiles\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the upgrade title text', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
expect(screen.getByText(/uploadMultipleFiles\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the upgrade description text', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
expect(screen.getByText(/uploadMultipleFiles\.description/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the upgrade button', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call setShowPricingModal when upgrade button is clicked', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call setShowPricingModal without user interaction', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call setShowPricingModal on each button click', () => {
|
||||
render(<UpgradeCard />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should maintain rendering after rerender with same props', () => {
|
||||
const { rerender } = render(<UpgradeCard />)
|
||||
|
||||
rerender(<UpgradeCard />)
|
||||
|
||||
expect(screen.getByText(/uploadMultipleFiles\.title/i)).toBeInTheDocument()
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,66 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
|
||||
// Mock config to control web crawl feature flags
|
||||
vi.mock('@/config', () => ({
|
||||
ENABLE_WEBSITE_FIRECRAWL: true,
|
||||
ENABLE_WEBSITE_JINAREADER: true,
|
||||
ENABLE_WEBSITE_WATERCRAWL: false,
|
||||
}))
|
||||
|
||||
// Mock CSS module
|
||||
vi.mock('../../../index.module.css', () => ({
|
||||
default: {
|
||||
dataSourceItem: 'ds-item',
|
||||
active: 'active',
|
||||
disabled: 'disabled',
|
||||
datasetIcon: 'icon',
|
||||
notion: 'notion-icon',
|
||||
web: 'web-icon',
|
||||
},
|
||||
}))
|
||||
|
||||
const { default: DataSourceTypeSelector } = await import('../data-source-type-selector')
|
||||
|
||||
describe('DataSourceTypeSelector', () => {
|
||||
const defaultProps = {
|
||||
currentType: DataSourceType.FILE,
|
||||
disabled: false,
|
||||
onChange: vi.fn(),
|
||||
onClearPreviews: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render file, notion, and web options', () => {
|
||||
render(<DataSourceTypeSelector {...defaultProps} />)
|
||||
expect(screen.getByText('datasetCreation.stepOne.dataSourceType.file')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.dataSourceType.notion')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.dataSourceType.web')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render as a 3-column grid', () => {
|
||||
const { container } = render(<DataSourceTypeSelector {...defaultProps} />)
|
||||
expect(container.firstElementChild).toHaveClass('grid-cols-3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('interactions', () => {
|
||||
it('should call onChange and onClearPreviews on type click', () => {
|
||||
render(<DataSourceTypeSelector {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(DataSourceType.NOTION)
|
||||
expect(defaultProps.onClearPreviews).toHaveBeenCalledWith(DataSourceType.NOTION)
|
||||
})
|
||||
|
||||
it('should not call onChange when disabled', () => {
|
||||
render(<DataSourceTypeSelector {...defaultProps} disabled />)
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
|
||||
expect(defaultProps.onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,48 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import NextStepButton from '../next-step-button'
|
||||
|
||||
describe('NextStepButton', () => {
|
||||
const defaultProps = {
|
||||
disabled: false,
|
||||
onClick: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render button text', () => {
|
||||
render(<NextStepButton {...defaultProps} />)
|
||||
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a primary variant button', () => {
|
||||
render(<NextStepButton {...defaultProps} />)
|
||||
const btn = screen.getByRole('button')
|
||||
expect(btn).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClick when clicked', () => {
|
||||
render(<NextStepButton {...defaultProps} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(defaultProps.onClick).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should be disabled when disabled prop is true', () => {
|
||||
render(<NextStepButton disabled onClick={defaultProps.onClick} />)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not call onClick when disabled', () => {
|
||||
render(<NextStepButton disabled onClick={defaultProps.onClick} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(defaultProps.onClick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render arrow icon', () => {
|
||||
const { container } = render(<NextStepButton {...defaultProps} />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,119 @@
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock child components - paths must match source file's imports (relative to source)
|
||||
vi.mock('../../../file-preview', () => ({
|
||||
default: ({ file, hidePreview }: { file: { name: string }, hidePreview: () => void }) => (
|
||||
<div data-testid="file-preview">
|
||||
<span>{file.name}</span>
|
||||
<button data-testid="close-file" onClick={hidePreview}>close-file</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../notion-page-preview', () => ({
|
||||
default: ({ currentPage, hidePreview }: { currentPage: { page_name: string }, hidePreview: () => void }) => (
|
||||
<div data-testid="notion-preview">
|
||||
<span>{currentPage.page_name}</span>
|
||||
<button data-testid="close-notion" onClick={hidePreview}>close-notion</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../website/preview', () => ({
|
||||
default: ({ payload, hidePreview }: { payload: { title: string }, hidePreview: () => void }) => (
|
||||
<div data-testid="website-preview">
|
||||
<span>{payload.title}</span>
|
||||
<button data-testid="close-website" onClick={hidePreview}>close-website</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
|
||||
default: ({ show, onClose, title }: { show: boolean, onClose: () => void, title: string }) => show
|
||||
? (
|
||||
<div data-testid="plan-upgrade-modal">
|
||||
<span>{title}</span>
|
||||
<button data-testid="close-modal" onClick={onClose}>close-modal</button>
|
||||
</div>
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
|
||||
const { default: PreviewPanel } = await import('../preview-panel')
|
||||
|
||||
describe('PreviewPanel', () => {
|
||||
const defaultProps = {
|
||||
currentFile: undefined,
|
||||
currentNotionPage: undefined,
|
||||
currentWebsite: undefined,
|
||||
notionCredentialId: 'cred-1',
|
||||
isShowPlanUpgradeModal: false,
|
||||
hideFilePreview: vi.fn(),
|
||||
hideNotionPagePreview: vi.fn(),
|
||||
hideWebsitePreview: vi.fn(),
|
||||
hidePlanUpgradeModal: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render nothing when no preview is active', () => {
|
||||
const { container } = render(<PreviewPanel {...defaultProps} />)
|
||||
expect(container.querySelector('[data-testid]')).toBeNull()
|
||||
})
|
||||
|
||||
it('should render file preview when currentFile is set', () => {
|
||||
render(<PreviewPanel {...defaultProps} currentFile={{ name: 'test.pdf' } as unknown as File} />)
|
||||
expect(screen.getByTestId('file-preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('test.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render notion preview when currentNotionPage is set', () => {
|
||||
render(<PreviewPanel {...defaultProps} currentNotionPage={{ page_name: 'My Page' } as unknown as NotionPage} />)
|
||||
expect(screen.getByTestId('notion-preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('My Page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render website preview when currentWebsite is set', () => {
|
||||
render(<PreviewPanel {...defaultProps} currentWebsite={{ title: 'My Site' } as unknown as CrawlResultItem} />)
|
||||
expect(screen.getByTestId('website-preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('My Site')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plan upgrade modal when isShowPlanUpgradeModal is true', () => {
|
||||
render(<PreviewPanel {...defaultProps} isShowPlanUpgradeModal />)
|
||||
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('interactions', () => {
|
||||
it('should call hideFilePreview when file preview close clicked', () => {
|
||||
render(<PreviewPanel {...defaultProps} currentFile={{ name: 'test.pdf' } as unknown as File} />)
|
||||
fireEvent.click(screen.getByTestId('close-file'))
|
||||
expect(defaultProps.hideFilePreview).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call hidePlanUpgradeModal when modal close clicked', () => {
|
||||
render(<PreviewPanel {...defaultProps} isShowPlanUpgradeModal />)
|
||||
fireEvent.click(screen.getByTestId('close-modal'))
|
||||
expect(defaultProps.hidePlanUpgradeModal).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call hideNotionPagePreview when notion preview close clicked', () => {
|
||||
render(<PreviewPanel {...defaultProps} currentNotionPage={{ page_name: 'My Page' } as unknown as NotionPage} />)
|
||||
fireEvent.click(screen.getByTestId('close-notion'))
|
||||
expect(defaultProps.hideNotionPagePreview).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call hideWebsitePreview when website preview close clicked', () => {
|
||||
render(<PreviewPanel {...defaultProps} currentWebsite={{ title: 'My Site' } as unknown as CrawlResultItem} />)
|
||||
fireEvent.click(screen.getByTestId('close-website'))
|
||||
expect(defaultProps.hideWebsitePreview).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,60 @@
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import usePreviewState from '../use-preview-state'
|
||||
|
||||
describe('usePreviewState', () => {
|
||||
it('should initialize with all previews undefined', () => {
|
||||
const { result } = renderHook(() => usePreviewState())
|
||||
|
||||
expect(result.current.currentFile).toBeUndefined()
|
||||
expect(result.current.currentNotionPage).toBeUndefined()
|
||||
expect(result.current.currentWebsite).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should show and hide file preview', () => {
|
||||
const { result } = renderHook(() => usePreviewState())
|
||||
const file = new File(['content'], 'test.pdf')
|
||||
|
||||
act(() => {
|
||||
result.current.showFilePreview(file)
|
||||
})
|
||||
expect(result.current.currentFile).toBe(file)
|
||||
|
||||
act(() => {
|
||||
result.current.hideFilePreview()
|
||||
})
|
||||
expect(result.current.currentFile).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should show and hide notion page preview', () => {
|
||||
const { result } = renderHook(() => usePreviewState())
|
||||
const page = { page_id: 'p1', page_name: 'Test' } as unknown as NotionPage
|
||||
|
||||
act(() => {
|
||||
result.current.showNotionPagePreview(page)
|
||||
})
|
||||
expect(result.current.currentNotionPage).toBe(page)
|
||||
|
||||
act(() => {
|
||||
result.current.hideNotionPagePreview()
|
||||
})
|
||||
expect(result.current.currentNotionPage).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should show and hide website preview', () => {
|
||||
const { result } = renderHook(() => usePreviewState())
|
||||
const website = { title: 'Example', source_url: 'https://example.com' } as unknown as CrawlResultItem
|
||||
|
||||
act(() => {
|
||||
result.current.showWebsitePreview(website)
|
||||
})
|
||||
expect(result.current.currentWebsite).toBe(website)
|
||||
|
||||
act(() => {
|
||||
result.current.hideWebsitePreview()
|
||||
})
|
||||
expect(result.current.currentWebsite).toBeUndefined()
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,10 @@
|
||||
import type { createDocumentResponse, FullDocumentDetail, IconInfo } from '@/models/datasets'
|
||||
import type { createDocumentResponse, DataSet, FullDocumentDetail, IconInfo } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import StepThree from './index'
|
||||
import StepThree from '../index'
|
||||
|
||||
// Mock the EmbeddingProcess component since it has complex async logic
|
||||
vi.mock('../embedding-process', () => ({
|
||||
vi.mock('../../embedding-process', () => ({
|
||||
default: vi.fn(({ datasetId, batchId, documents, indexingType, retrievalMethod }) => (
|
||||
<div data-testid="embedding-process">
|
||||
<span data-testid="ep-dataset-id">{datasetId}</span>
|
||||
@ -98,97 +98,74 @@ const renderStepThree = (props: Partial<Parameters<typeof StepThree>[0]> = {}) =
|
||||
return render(<StepThree {...defaultProps} />)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// StepThree Component Tests
|
||||
// ============================================================================
|
||||
describe('StepThree', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockMediaType = 'pc'
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests - Verify component renders properly
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with creation title when datasetId is not provided', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.creationTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepThree.creationContent')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with addition title when datasetId is provided', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({
|
||||
datasetId: 'existing-dataset-123',
|
||||
datasetName: 'Existing Dataset',
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.additionTitle')).toBeInTheDocument()
|
||||
expect(screen.queryByText('datasetCreation.stepThree.creationTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render label text in creation mode', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render side tip panel on desktop', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.sideTipTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepThree.sideTipContent')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render side tip panel on mobile', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'mobile'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepThree.sideTipTitle')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('datasetCreation.stepThree.sideTipContent')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render EmbeddingProcess component', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render documentation link with correct href on desktop', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')
|
||||
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/use-dify/knowledge/integrate-knowledge-within-application')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
@ -196,70 +173,53 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should apply correct container classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const outerDiv = container.firstChild as HTMLElement
|
||||
expect(outerDiv).toHaveClass('flex', 'h-full', 'max-h-full', 'w-full', 'justify-center', 'overflow-y-auto')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing - Test all prop variations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('datasetId prop', () => {
|
||||
it('should render creation mode when datasetId is undefined', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetId: undefined })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.creationTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render addition mode when datasetId is provided', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetId: 'dataset-123' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.additionTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass datasetId to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
const datasetId = 'my-dataset-id'
|
||||
|
||||
// Act
|
||||
renderStepThree({ datasetId })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent(datasetId)
|
||||
})
|
||||
|
||||
it('should use creationCache dataset id when datasetId is not provided', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('dataset-123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('datasetName prop', () => {
|
||||
it('should display datasetName in creation mode', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetName: 'My Custom Dataset' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('My Custom Dataset')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display datasetName in addition mode description', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({
|
||||
datasetId: 'dataset-123',
|
||||
datasetName: 'Existing Dataset Name',
|
||||
@ -271,45 +231,35 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should fallback to creationCache dataset name when datasetName is not provided', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.name = 'Cache Dataset Name'
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Cache Dataset Name')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('indexingType prop', () => {
|
||||
it('should pass indexingType to EmbeddingProcess', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ indexingType: 'high_quality' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-indexing-type')).toHaveTextContent('high_quality')
|
||||
})
|
||||
|
||||
it('should use creationCache indexing_technique when indexingType is not provided', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.indexing_technique = 'economy' as any
|
||||
creationCache.dataset!.indexing_technique = 'economy' as unknown as DataSet['indexing_technique']
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-indexing-type')).toHaveTextContent('economy')
|
||||
})
|
||||
|
||||
it('should prefer creationCache indexing_technique over indexingType prop', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.indexing_technique = 'cache_technique' as any
|
||||
creationCache.dataset!.indexing_technique = 'cache_technique' as unknown as DataSet['indexing_technique']
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache, indexingType: 'prop_technique' })
|
||||
|
||||
// Assert - creationCache takes precedence
|
||||
@ -319,60 +269,47 @@ describe('StepThree', () => {
|
||||
|
||||
describe('retrievalMethod prop', () => {
|
||||
it('should pass retrievalMethod to EmbeddingProcess', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ retrievalMethod: RETRIEVE_METHOD.semantic })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-retrieval-method')).toHaveTextContent('semantic_search')
|
||||
})
|
||||
|
||||
it('should use creationCache retrieval method when retrievalMethod is not provided', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.retrieval_model_dict = { search_method: 'full_text_search' } as any
|
||||
creationCache.dataset!.retrieval_model_dict = { search_method: 'full_text_search' } as unknown as DataSet['retrieval_model_dict']
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-retrieval-method')).toHaveTextContent('full_text_search')
|
||||
})
|
||||
})
|
||||
|
||||
describe('creationCache prop', () => {
|
||||
it('should pass batchId from creationCache to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.batch = 'custom-batch-123'
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('custom-batch-123')
|
||||
})
|
||||
|
||||
it('should pass documents from creationCache to EmbeddingProcess', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.documents = [createMockDocument(), createMockDocument(), createMockDocument()] as any
|
||||
creationCache.documents = [createMockDocument(), createMockDocument(), createMockDocument()] as unknown as createDocumentResponse['documents']
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('3')
|
||||
})
|
||||
|
||||
it('should use icon_info from creationCache dataset', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.icon_info = createMockIconInfo({
|
||||
icon: '🚀',
|
||||
icon_background: '#FF0000',
|
||||
})
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree({ creationCache })
|
||||
|
||||
// Assert - Check AppIcon component receives correct props
|
||||
@ -381,7 +318,6 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle undefined creationCache', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ creationCache: undefined })
|
||||
|
||||
// Assert - Should not crash, use fallback values
|
||||
@ -390,14 +326,12 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle creationCache with undefined dataset', () => {
|
||||
// Arrange
|
||||
const creationCache: createDocumentResponse = {
|
||||
dataset: undefined,
|
||||
batch: 'batch-123',
|
||||
documents: [],
|
||||
}
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert - Should use default icon info
|
||||
@ -406,12 +340,9 @@ describe('StepThree', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases Tests - Test null, undefined, empty values and boundaries
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle all props being undefined', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({
|
||||
datasetId: undefined,
|
||||
datasetName: undefined,
|
||||
@ -426,7 +357,6 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle empty string datasetId', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetId: '' })
|
||||
|
||||
// Assert - Empty string is falsy, should show creation mode
|
||||
@ -434,7 +364,6 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle empty string datasetName', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetName: '' })
|
||||
|
||||
// Assert - Should not crash
|
||||
@ -442,23 +371,18 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle empty documents array in creationCache', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.documents = []
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('0')
|
||||
})
|
||||
|
||||
it('should handle creationCache with missing icon_info', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.icon_info = undefined as any
|
||||
creationCache.dataset!.icon_info = undefined as unknown as IconInfo
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert - Should use default icon info
|
||||
@ -466,10 +390,8 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle very long datasetName', () => {
|
||||
// Arrange
|
||||
const longName = 'A'.repeat(500)
|
||||
|
||||
// Act
|
||||
renderStepThree({ datasetName: longName })
|
||||
|
||||
// Assert - Should render without crashing
|
||||
@ -477,10 +399,8 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle special characters in datasetName', () => {
|
||||
// Arrange
|
||||
const specialName = 'Dataset <script>alert("xss")</script> & "quotes" \'apostrophe\''
|
||||
|
||||
// Act
|
||||
renderStepThree({ datasetName: specialName })
|
||||
|
||||
// Assert - Should render safely as text
|
||||
@ -488,22 +408,17 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should handle unicode characters in datasetName', () => {
|
||||
// Arrange
|
||||
const unicodeName = '数据集名称 🚀 émojis & spëcîal çhàrs'
|
||||
|
||||
// Act
|
||||
renderStepThree({ datasetName: unicodeName })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(unicodeName)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle creationCache with null dataset name', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.name = null as any
|
||||
creationCache.dataset!.name = null as unknown as string
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree({ creationCache })
|
||||
|
||||
// Assert - Should not crash
|
||||
@ -511,13 +426,10 @@ describe('StepThree', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Conditional Rendering Tests - Test mode switching behavior
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Conditional Rendering', () => {
|
||||
describe('Creation Mode (no datasetId)', () => {
|
||||
it('should show AppIcon component', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert - AppIcon should be rendered
|
||||
@ -526,7 +438,6 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should show Divider component', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert - Divider should be rendered (it adds hr with specific classes)
|
||||
@ -535,20 +446,16 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should show dataset name input area', () => {
|
||||
// Arrange
|
||||
const datasetName = 'Test Dataset Name'
|
||||
|
||||
// Act
|
||||
renderStepThree({ datasetName })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(datasetName)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Addition Mode (with datasetId)', () => {
|
||||
it('should not show AppIcon component', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetId: 'dataset-123' })
|
||||
|
||||
// Assert - Creation section should not be rendered
|
||||
@ -556,7 +463,6 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should show addition description with dataset name', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({
|
||||
datasetId: 'dataset-123',
|
||||
datasetName: 'My Dataset',
|
||||
@ -569,10 +475,8 @@ describe('StepThree', () => {
|
||||
|
||||
describe('Mobile vs Desktop', () => {
|
||||
it('should show side panel on tablet', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'tablet'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert - Tablet is not mobile, should show side panel
|
||||
@ -580,21 +484,16 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should not show side panel on mobile', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'mobile'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepThree.sideTipTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render EmbeddingProcess on mobile', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'mobile'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert - Main content should still be rendered
|
||||
@ -603,64 +502,48 @@ describe('StepThree', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// EmbeddingProcess Integration Tests - Verify correct props are passed
|
||||
// --------------------------------------------------------------------------
|
||||
describe('EmbeddingProcess Integration', () => {
|
||||
it('should pass correct datasetId to EmbeddingProcess with datasetId prop', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetId: 'direct-dataset-id' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('direct-dataset-id')
|
||||
})
|
||||
|
||||
it('should pass creationCache dataset id when datasetId prop is undefined', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.id = 'cache-dataset-id'
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('cache-dataset-id')
|
||||
})
|
||||
|
||||
it('should pass empty string for datasetId when both sources are undefined', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should pass batchId from creationCache', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.batch = 'test-batch-456'
|
||||
|
||||
// Act
|
||||
renderStepThree({ creationCache })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('test-batch-456')
|
||||
})
|
||||
|
||||
it('should pass empty string for batchId when creationCache is undefined', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should prefer datasetId prop over creationCache dataset id', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.id = 'cache-id'
|
||||
|
||||
// Act
|
||||
renderStepThree({ datasetId: 'prop-id', creationCache })
|
||||
|
||||
// Assert - datasetId prop takes precedence
|
||||
@ -668,12 +551,9 @@ describe('StepThree', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Icon Rendering Tests - Verify AppIcon behavior
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Icon Rendering', () => {
|
||||
it('should use default icon info when creationCache is undefined', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert - Default background color should be applied
|
||||
@ -683,7 +563,6 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should use icon_info from creationCache when available', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
creationCache.dataset!.icon_info = {
|
||||
icon: '🎉',
|
||||
@ -692,7 +571,6 @@ describe('StepThree', () => {
|
||||
icon_url: '',
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree({ creationCache })
|
||||
|
||||
// Assert - Custom background color should be applied
|
||||
@ -702,11 +580,9 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should use default icon when creationCache dataset icon_info is undefined', () => {
|
||||
// Arrange
|
||||
const creationCache = createMockCreationCache()
|
||||
delete (creationCache.dataset as any).icon_info
|
||||
delete (creationCache.dataset as Partial<DataSet>).icon_info
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree({ creationCache })
|
||||
|
||||
// Assert - Component should still render with default icon
|
||||
@ -714,15 +590,11 @@ describe('StepThree', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Layout Tests - Verify correct CSS classes and structure
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Layout', () => {
|
||||
it('should have correct outer container classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const outerDiv = container.firstChild as HTMLElement
|
||||
expect(outerDiv).toHaveClass('flex')
|
||||
expect(outerDiv).toHaveClass('h-full')
|
||||
@ -730,49 +602,37 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should have correct inner container classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const innerDiv = container.querySelector('.max-w-\\[960px\\]')
|
||||
expect(innerDiv).toBeInTheDocument()
|
||||
expect(innerDiv).toHaveClass('shrink-0', 'grow')
|
||||
})
|
||||
|
||||
it('should have content wrapper with correct max width', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const contentWrapper = container.querySelector('.max-w-\\[640px\\]')
|
||||
expect(contentWrapper).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have side tip panel with correct width on desktop', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const sidePanel = container.querySelector('.w-\\[328px\\]')
|
||||
expect(sidePanel).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Accessibility Tests - Verify accessibility features
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have correct link attributes for external documentation link', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')
|
||||
expect(link.tagName).toBe('A')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
@ -780,35 +640,27 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should have semantic heading structure in creation mode', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree()
|
||||
|
||||
// Assert
|
||||
const title = screen.getByText('datasetCreation.stepThree.creationTitle')
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title.className).toContain('title-2xl-semi-bold')
|
||||
})
|
||||
|
||||
it('should have semantic heading structure in addition mode', () => {
|
||||
// Arrange & Act
|
||||
renderStepThree({ datasetId: 'dataset-123' })
|
||||
|
||||
// Assert
|
||||
const title = screen.getByText('datasetCreation.stepThree.additionTitle')
|
||||
expect(title).toBeInTheDocument()
|
||||
expect(title.className).toContain('title-2xl-semi-bold')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Side Panel Tests - Verify side panel behavior
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Side Panel', () => {
|
||||
it('should render RiBookOpenLine icon in side panel', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert - Icon should be present in side panel
|
||||
@ -817,25 +669,19 @@ describe('StepThree', () => {
|
||||
})
|
||||
|
||||
it('should have correct side panel section background', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const sidePanel = container.querySelector('.bg-background-section')
|
||||
expect(sidePanel).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct padding for side panel', () => {
|
||||
// Arrange
|
||||
mockMediaType = 'pc'
|
||||
|
||||
// Act
|
||||
const { container } = renderStepThree()
|
||||
|
||||
// Assert
|
||||
const sidePanelWrapper = container.querySelector('.pr-8')
|
||||
expect(sidePanelWrapper).toBeInTheDocument()
|
||||
})
|
||||
@ -10,12 +10,12 @@ import type {
|
||||
Rules,
|
||||
} from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { act, fireEvent, render, renderHook, screen } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
|
||||
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ChunkingMode, DataSourceType, ProcessMode } from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { PreviewPanel } from './components/preview-panel'
|
||||
import { StepTwoFooter } from './components/step-two-footer'
|
||||
import { PreviewPanel } from '../components/preview-panel'
|
||||
import { StepTwoFooter } from '../components/step-two-footer'
|
||||
import {
|
||||
DEFAULT_MAXIMUM_CHUNK_LENGTH,
|
||||
DEFAULT_OVERLAP,
|
||||
@ -27,15 +27,11 @@ import {
|
||||
useIndexingEstimate,
|
||||
usePreviewState,
|
||||
useSegmentationState,
|
||||
} from './hooks'
|
||||
import escape from './hooks/escape'
|
||||
import unescape from './hooks/unescape'
|
||||
} from '../hooks'
|
||||
import escape from '../hooks/escape'
|
||||
import unescape from '../hooks/unescape'
|
||||
import StepTwo from '../index'
|
||||
|
||||
// ============================================
|
||||
// Mock external dependencies
|
||||
// ============================================
|
||||
|
||||
// Mock dataset detail context
|
||||
const mockDataset = {
|
||||
id: 'test-dataset-id',
|
||||
doc_form: ChunkingMode.text,
|
||||
@ -60,10 +56,6 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
selector({ dataset: mockCurrentDataset, mutateDatasetRes: mockMutateDatasetRes }),
|
||||
}))
|
||||
|
||||
// Note: @/context/i18n is globally mocked in vitest.setup.ts, no need to mock here
|
||||
// Note: @/hooks/use-breakpoints uses real import
|
||||
|
||||
// Mock model hooks
|
||||
const mockEmbeddingModelList = [
|
||||
{ provider: 'openai', model: 'text-embedding-ada-002' },
|
||||
{ provider: 'cohere', model: 'embed-english-v3.0' },
|
||||
@ -99,7 +91,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
||||
useDefaultModel: () => ({ data: mockDefaultEmbeddingModel }),
|
||||
}))
|
||||
|
||||
// Mock service hooks
|
||||
const mockFetchDefaultProcessRuleMutate = vi.fn()
|
||||
vi.mock('@/service/knowledge/use-create-dataset', () => ({
|
||||
useFetchDefaultProcessRule: ({ onSuccess }: { onSuccess: (data: { rules: Rules, limits: { indexing_max_segmentation_tokens_length: number } }) => void }) => ({
|
||||
@ -170,18 +161,55 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
// Note: @/app/components/base/toast - uses real import (base component)
|
||||
// Note: @/app/components/datasets/common/check-rerank-model - uses real import
|
||||
// Note: @/app/components/base/float-right-container - uses real import (base component)
|
||||
// Enable IS_CE_EDITION to show QA checkbox in tests
|
||||
vi.mock('@/config', async () => {
|
||||
const actual = await vi.importActual('@/config')
|
||||
return { ...actual, IS_CE_EDITION: true }
|
||||
})
|
||||
|
||||
// Mock PreviewDocumentPicker to allow testing handlePickerChange
|
||||
vi.mock('@/app/components/datasets/common/document-picker/preview-document-picker', () => ({
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
default: ({ onChange, value, files }: { onChange: (item: any) => void, value: any, files: any[] }) => (
|
||||
<div data-testid="preview-picker">
|
||||
<span>{value?.name}</span>
|
||||
{files?.map((f: { id: string, name: string }) => (
|
||||
<button key={f.id} data-testid={`picker-${f.id}`} onClick={() => onChange(f)}>
|
||||
{f.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock checkShowMultiModalTip - requires complex model list structure
|
||||
vi.mock('@/app/components/datasets/settings/utils', () => ({
|
||||
checkShowMultiModalTip: () => false,
|
||||
}))
|
||||
|
||||
// ============================================
|
||||
// Test data factories
|
||||
// ============================================
|
||||
// Mock complex child components to avoid deep dependency chains when rendering StepTwo
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
default: ({ onSelect, readonly }: { onSelect?: (val: Record<string, string>) => void, readonly?: boolean }) => (
|
||||
<div data-testid="model-selector" data-readonly={readonly}>
|
||||
<button onClick={() => onSelect?.({ provider: 'openai', model: 'text-embedding-3-small' })}>Select Model</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/retrieval-method-config', () => ({
|
||||
default: ({ disabled }: { disabled?: boolean }) => (
|
||||
<div data-testid="retrieval-method-config" data-disabled={disabled}>
|
||||
Retrieval Config
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/economical-retrieval-method-config', () => ({
|
||||
default: ({ disabled }: { disabled?: boolean }) => (
|
||||
<div data-testid="economical-retrieval-config" data-disabled={disabled}>
|
||||
Economical Config
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockFile = (overrides?: Partial<CustomFile>): CustomFile => ({
|
||||
id: 'file-1',
|
||||
@ -248,9 +276,7 @@ const createMockEstimate = (overrides?: Partial<FileIndexingEstimateResponse>):
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// Utility Functions Tests (escape/unescape)
|
||||
// ============================================
|
||||
|
||||
describe('escape utility', () => {
|
||||
beforeEach(() => {
|
||||
@ -371,10 +397,6 @@ describe('unescape utility', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// useSegmentationState Hook Tests
|
||||
// ============================================
|
||||
|
||||
describe('useSegmentationState', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -713,9 +735,7 @@ describe('useSegmentationState', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// useIndexingConfig Hook Tests
|
||||
// ============================================
|
||||
|
||||
describe('useIndexingConfig', () => {
|
||||
beforeEach(() => {
|
||||
@ -887,9 +907,7 @@ describe('useIndexingConfig', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// usePreviewState Hook Tests
|
||||
// ============================================
|
||||
|
||||
describe('usePreviewState', () => {
|
||||
beforeEach(() => {
|
||||
@ -1116,9 +1134,7 @@ describe('usePreviewState', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// useDocumentCreation Hook Tests
|
||||
// ============================================
|
||||
|
||||
describe('useDocumentCreation', () => {
|
||||
beforeEach(() => {
|
||||
@ -1540,9 +1556,7 @@ describe('useDocumentCreation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// useIndexingEstimate Hook Tests
|
||||
// ============================================
|
||||
|
||||
describe('useIndexingEstimate', () => {
|
||||
beforeEach(() => {
|
||||
@ -1682,9 +1696,7 @@ describe('useIndexingEstimate', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// StepTwoFooter Component Tests
|
||||
// ============================================
|
||||
|
||||
describe('StepTwoFooter', () => {
|
||||
beforeEach(() => {
|
||||
@ -1774,9 +1786,7 @@ describe('StepTwoFooter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// PreviewPanel Component Tests
|
||||
// ============================================
|
||||
|
||||
describe('PreviewPanel', () => {
|
||||
beforeEach(() => {
|
||||
@ -1955,10 +1965,6 @@ describe('PreviewPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// Edge Cases Tests
|
||||
// ============================================
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -2072,9 +2078,7 @@ describe('Edge Cases', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// Integration Scenarios
|
||||
// ============================================
|
||||
|
||||
describe('Integration Scenarios', () => {
|
||||
beforeEach(() => {
|
||||
@ -2195,3 +2199,357 @@ describe('Integration Scenarios', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// StepTwo Component Tests
|
||||
|
||||
describe('StepTwo Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCurrentDataset = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
const defaultStepTwoProps = {
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
files: [createMockFile()],
|
||||
isAPIKeySet: true,
|
||||
onSetting: vi.fn(),
|
||||
notionCredentialId: '',
|
||||
onStepChange: vi.fn(),
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show general chunking options when not in upload', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// Should render the segmentation section
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show footer with Previous and Next buttons', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
expect(screen.getByText(/stepTwo\.previousStep/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/stepTwo\.nextStep/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should fetch default process rule when not in setting mode', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
expect(mockFetchDefaultProcessRuleMutate).toHaveBeenCalledWith('/datasets/process-rule')
|
||||
})
|
||||
|
||||
it('should apply config from rules when in setting mode with document detail', () => {
|
||||
const docDetail = createMockDocumentDetail()
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
isSetting={true}
|
||||
documentDetail={docDetail}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// Should not fetch default rule when isSetting
|
||||
expect(mockFetchDefaultProcessRuleMutate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onStepChange(-1) when Previous button is clicked', () => {
|
||||
const onStepChange = vi.fn()
|
||||
render(<StepTwo {...defaultStepTwoProps} onStepChange={onStepChange} />)
|
||||
fireEvent.click(screen.getByText(/stepTwo\.previousStep/i))
|
||||
expect(onStepChange).toHaveBeenCalledWith(-1)
|
||||
})
|
||||
|
||||
it('should trigger handleCreate when Next Step button is clicked', async () => {
|
||||
const onStepChange = vi.fn()
|
||||
render(<StepTwo {...defaultStepTwoProps} onStepChange={onStepChange} />)
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText(/stepTwo\.nextStep/i))
|
||||
})
|
||||
// handleCreate validates, builds params, and calls executeCreation
|
||||
// which calls onStepChange(1) on success
|
||||
expect(onStepChange).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('should trigger updatePreview when preview button is clicked', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// GeneralChunkingOptions renders a "Preview Chunk" button
|
||||
const previewButtons = screen.getAllByText(/stepTwo\.previewChunk/i)
|
||||
fireEvent.click(previewButtons[0])
|
||||
// updatePreview calls estimateHook.fetchEstimate()
|
||||
// No error means the handler executed successfully
|
||||
})
|
||||
|
||||
it('should trigger handleDocFormChange through parent-child option switch', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// ParentChildOptions renders an OptionCard; find the title element and click its parent card
|
||||
const parentChildTitles = screen.getAllByText(/stepTwo\.parentChild/i)
|
||||
// The first match is the title; click it to trigger onDocFormChange
|
||||
fireEvent.click(parentChildTitles[0])
|
||||
// handleDocFormChange sets docForm, segmentationType, and resets estimate
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering', () => {
|
||||
it('should show options based on currentDataset doc_form', () => {
|
||||
mockCurrentDataset = { ...mockDataset, doc_form: ChunkingMode.parentChild }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// When currentDataset has parentChild doc_form, should show parent-child option
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render setting mode with Save/Cancel buttons', () => {
|
||||
const docDetail = createMockDocumentDetail()
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
isSetting={true}
|
||||
documentDetail={docDetail}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText(/stepTwo\.save/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/stepTwo\.cancel/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onCancel when Cancel button is clicked in setting mode', () => {
|
||||
const onCancel = vi.fn()
|
||||
const docDetail = createMockDocumentDetail()
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
isSetting={true}
|
||||
documentDetail={docDetail}
|
||||
datasetId="test-id"
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
fireEvent.click(screen.getByText(/stepTwo\.cancel/i))
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should trigger handleCreate (Save) in setting mode', async () => {
|
||||
const onSave = vi.fn()
|
||||
const docDetail = createMockDocumentDetail()
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
isSetting={true}
|
||||
documentDetail={docDetail}
|
||||
datasetId="test-id"
|
||||
onSave={onSave}
|
||||
/>,
|
||||
)
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText(/stepTwo\.save/i))
|
||||
})
|
||||
// handleCreate → validateParams → buildCreationParams → executeCreation → onSave
|
||||
expect(onSave).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show both general and parent-child options in create page', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// When isInInit (no datasetId, no isSetting), both options should show
|
||||
expect(screen.getByText('datasetCreation.stepTwo.general')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepTwo.parentChild')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should only show parent-child option when dataset has parentChild doc_form', () => {
|
||||
mockCurrentDataset = { ...mockDataset, doc_form: ChunkingMode.parentChild }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// showGeneralOption should be false (parentChild not in [text, qa])
|
||||
// showParentChildOption should be true
|
||||
expect(screen.getByText('datasetCreation.stepTwo.parentChild')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show general option only when dataset has text doc_form', () => {
|
||||
mockCurrentDataset = { ...mockDataset, doc_form: ChunkingMode.text }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// showGeneralOption should be true (text is in [text, qa])
|
||||
expect(screen.getByText('datasetCreation.stepTwo.general')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Upload in Dataset', () => {
|
||||
it('should show general option when in upload with text doc_form', () => {
|
||||
mockCurrentDataset = { ...mockDataset, doc_form: ChunkingMode.text }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show general option for empty dataset (no doc_form)', () => {
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
mockCurrentDataset = { ...mockDataset, doc_form: undefined as any }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show both options in empty dataset upload', () => {
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
mockCurrentDataset = { ...mockDataset, doc_form: undefined as any }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// isUploadInEmptyDataset=true shows both options
|
||||
expect(screen.getByText('datasetCreation.stepTwo.general')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepTwo.parentChild')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Indexing Mode', () => {
|
||||
it('should render indexing mode section', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// IndexingModeSection renders the index mode title
|
||||
expect(screen.getByText(/stepTwo\.indexMode/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render embedding model selector when QUALIFIED', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// ModelSelector is mocked and rendered with data-testid
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render retrieval method config', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// RetrievalMethodConfig is mocked with data-testid
|
||||
expect(screen.getByTestId('retrieval-method-config')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable model and retrieval config when datasetId has existing data source', () => {
|
||||
mockCurrentDataset = { ...mockDataset, data_source_type: DataSourceType.FILE }
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// isModelAndRetrievalConfigDisabled should be true
|
||||
const modelSelector = screen.getByTestId('model-selector')
|
||||
expect(modelSelector).toHaveAttribute('data-readonly', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Preview Panel', () => {
|
||||
it('should render preview panel', () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide document picker in setting mode', () => {
|
||||
const docDetail = createMockDocumentDetail()
|
||||
render(
|
||||
<StepTwo
|
||||
{...defaultStepTwoProps}
|
||||
isSetting={true}
|
||||
documentDetail={docDetail}
|
||||
datasetId="test-id"
|
||||
/>,
|
||||
)
|
||||
// Preview panel should still render
|
||||
expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Handler Functions - Uncovered Paths', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCurrentDataset = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('should switch to QUALIFIED when selecting parentChild in ECONOMICAL mode', async () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} isAPIKeySet={false} />)
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
const parentChildTitles = screen.getAllByText(/stepTwo\.parentChild/i)
|
||||
fireEvent.click(parentChildTitles[0])
|
||||
})
|
||||
|
||||
it('should open QA confirm dialog and confirm switch when QA selected in ECONOMICAL mode', async () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} isAPIKeySet={false} />)
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i)
|
||||
fireEvent.click(qaCheckbox)
|
||||
// Dialog should open → click Switch to confirm (triggers handleQAConfirm)
|
||||
const switchButton = await screen.findByText(/stepTwo\.switch/i)
|
||||
expect(switchButton).toBeInTheDocument()
|
||||
fireEvent.click(switchButton)
|
||||
})
|
||||
|
||||
it('should close QA confirm dialog when cancel is clicked', async () => {
|
||||
render(<StepTwo {...defaultStepTwoProps} isAPIKeySet={false} />)
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
|
||||
})
|
||||
// Open QA confirm dialog
|
||||
const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i)
|
||||
fireEvent.click(qaCheckbox)
|
||||
const dialogCancelButtons = await screen.findAllByText(/stepTwo\.cancel/i)
|
||||
fireEvent.click(dialogCancelButtons[0])
|
||||
})
|
||||
|
||||
it('should handle picker change when selecting a different file', () => {
|
||||
const files = [
|
||||
createMockFile({ id: 'file-1', name: 'first.pdf', extension: 'pdf' }),
|
||||
createMockFile({ id: 'file-2', name: 'second.pdf', extension: 'pdf' }),
|
||||
]
|
||||
render(<StepTwo {...defaultStepTwoProps} files={files} />)
|
||||
const pickerButton = screen.getByTestId('picker-file-2')
|
||||
fireEvent.click(pickerButton)
|
||||
})
|
||||
|
||||
it('should show error toast when preview is clicked with maxChunkLength exceeding limit', () => {
|
||||
// Set a high maxChunkLength via the DOM attribute
|
||||
document.body.setAttribute('data-public-indexing-max-segmentation-tokens-length', '100')
|
||||
render(<StepTwo {...defaultStepTwoProps} />)
|
||||
// The default maxChunkLength (1024) now exceeds the limit (100)
|
||||
const previewButtons = screen.getAllByText(/stepTwo\.previewChunk/i)
|
||||
fireEvent.click(previewButtons[0])
|
||||
// Restore
|
||||
document.body.removeAttribute('data-public-indexing-max-segmentation-tokens-length')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,168 @@
|
||||
import type { PreProcessingRule } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { GeneralChunkingOptions } from '../general-chunking-options'
|
||||
|
||||
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
||||
default: ({ onSummaryIndexSettingChange }: { onSummaryIndexSettingChange?: (val: Record<string, unknown>) => void }) => (
|
||||
<div data-testid="summary-index-setting">
|
||||
<button data-testid="summary-toggle" onClick={() => onSummaryIndexSettingChange?.({ enable: true })}>Toggle</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
IS_CE_EDITION: true,
|
||||
}))
|
||||
|
||||
const ns = 'datasetCreation'
|
||||
|
||||
const createRules = (): PreProcessingRule[] => [
|
||||
{ id: 'remove_extra_spaces', enabled: true },
|
||||
{ id: 'remove_urls_emails', enabled: false },
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
segmentIdentifier: '\\n',
|
||||
maxChunkLength: 500,
|
||||
overlap: 50,
|
||||
rules: createRules(),
|
||||
currentDocForm: ChunkingMode.text,
|
||||
docLanguage: 'English',
|
||||
isActive: true,
|
||||
isInUpload: false,
|
||||
isNotUploadInEmptyDataset: false,
|
||||
hasCurrentDatasetDocForm: false,
|
||||
onSegmentIdentifierChange: vi.fn(),
|
||||
onMaxChunkLengthChange: vi.fn(),
|
||||
onOverlapChange: vi.fn(),
|
||||
onRuleToggle: vi.fn(),
|
||||
onDocFormChange: vi.fn(),
|
||||
onDocLanguageChange: vi.fn(),
|
||||
onPreview: vi.fn(),
|
||||
onReset: vi.fn(),
|
||||
locale: 'en',
|
||||
}
|
||||
|
||||
describe('GeneralChunkingOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render general chunking title', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.general`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render delimiter, max length and overlap inputs when active', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.separator`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.maxLength`)).toBeInTheDocument()
|
||||
expect(screen.getAllByText(new RegExp(`${ns}.stepTwo.overlap`)).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render preprocessing rules as checkboxes', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.removeExtraSpaces`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.removeUrlEmails`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render preview and reset buttons when active', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.previewChunk`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.reset`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render body when not active', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} isActive={false} />)
|
||||
expect(screen.queryByText(`${ns}.stepTwo.separator`)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onPreview when preview button clicked', () => {
|
||||
const onPreview = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} onPreview={onPreview} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.previewChunk`))
|
||||
expect(onPreview).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onReset when reset button clicked', () => {
|
||||
const onReset = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} onReset={onReset} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.reset`))
|
||||
expect(onReset).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onRuleToggle when rule clicked', () => {
|
||||
const onRuleToggle = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} onRuleToggle={onRuleToggle} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.removeUrlEmails`))
|
||||
expect(onRuleToggle).toHaveBeenCalledWith('remove_urls_emails')
|
||||
})
|
||||
|
||||
it('should call onDocFormChange with text mode when card switched', () => {
|
||||
const onDocFormChange = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} isActive={false} onDocFormChange={onDocFormChange} />)
|
||||
// OptionCard fires onSwitched which calls onDocFormChange(ChunkingMode.text)
|
||||
// Since isActive=false, clicking the card triggers the switch
|
||||
const titleEl = screen.getByText(`${ns}.stepTwo.general`)
|
||||
fireEvent.click(titleEl.closest('[class*="rounded-xl"]')!)
|
||||
expect(onDocFormChange).toHaveBeenCalledWith(ChunkingMode.text)
|
||||
})
|
||||
})
|
||||
|
||||
describe('QA Mode (CE Edition)', () => {
|
||||
it('should render QA language checkbox', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.useQALanguage`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle QA mode when checkbox clicked', () => {
|
||||
const onDocFormChange = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} onDocFormChange={onDocFormChange} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.useQALanguage`))
|
||||
expect(onDocFormChange).toHaveBeenCalledWith(ChunkingMode.qa)
|
||||
})
|
||||
|
||||
it('should toggle back to text mode from QA mode', () => {
|
||||
const onDocFormChange = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} currentDocForm={ChunkingMode.qa} onDocFormChange={onDocFormChange} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.useQALanguage`))
|
||||
expect(onDocFormChange).toHaveBeenCalledWith(ChunkingMode.text)
|
||||
})
|
||||
|
||||
it('should not toggle QA mode when hasCurrentDatasetDocForm is true', () => {
|
||||
const onDocFormChange = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} hasCurrentDatasetDocForm onDocFormChange={onDocFormChange} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.useQALanguage`))
|
||||
expect(onDocFormChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show QA warning tip when in QA mode', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} currentDocForm={ChunkingMode.qa} />)
|
||||
expect(screen.getAllByText(`${ns}.stepTwo.QATip`).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Summary Index Setting', () => {
|
||||
it('should render SummaryIndexSetting when showSummaryIndexSetting is true', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} showSummaryIndexSetting />)
|
||||
expect(screen.getByTestId('summary-index-setting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render SummaryIndexSetting when showSummaryIndexSetting is false', () => {
|
||||
render(<GeneralChunkingOptions {...defaultProps} showSummaryIndexSetting={false} />)
|
||||
expect(screen.queryByTestId('summary-index-setting')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onSummaryIndexSettingChange', () => {
|
||||
const onSummaryIndexSettingChange = vi.fn()
|
||||
render(<GeneralChunkingOptions {...defaultProps} showSummaryIndexSetting onSummaryIndexSettingChange={onSummaryIndexSettingChange} />)
|
||||
fireEvent.click(screen.getByTestId('summary-toggle'))
|
||||
expect(onSummaryIndexSettingChange).toHaveBeenCalledWith({ enable: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,213 @@
|
||||
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { IndexingType } from '../../hooks'
|
||||
import { IndexingModeSection } from '../indexing-mode-section'
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, ...props }: { children?: React.ReactNode, href?: string, className?: string }) => <a href={href} {...props}>{children}</a>,
|
||||
}))
|
||||
|
||||
// Mock external domain components
|
||||
vi.mock('@/app/components/datasets/common/retrieval-method-config', () => ({
|
||||
default: ({ onChange, disabled }: { value?: RetrievalConfig, onChange?: (val: Record<string, unknown>) => void, disabled?: boolean }) => (
|
||||
<div data-testid="retrieval-method-config" data-disabled={disabled}>
|
||||
<button onClick={() => onChange?.({ search_method: 'updated' })}>Change Retrieval</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/economical-retrieval-method-config', () => ({
|
||||
default: ({ disabled }: { value?: RetrievalConfig, onChange?: (val: Record<string, unknown>) => void, disabled?: boolean }) => (
|
||||
<div data-testid="economical-retrieval-config" data-disabled={disabled}>
|
||||
Economical Config
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
default: ({ onSelect, readonly }: { onSelect?: (val: Record<string, string>) => void, readonly?: boolean }) => (
|
||||
<div data-testid="model-selector" data-readonly={readonly}>
|
||||
<button onClick={() => onSelect?.({ provider: 'openai', model: 'text-embedding-3-small' })}>Select Model</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
const ns = 'datasetCreation'
|
||||
|
||||
const createDefaultModel = (overrides?: Partial<DefaultModel>): DefaultModel => ({
|
||||
provider: 'openai',
|
||||
model: 'text-embedding-ada-002',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createRetrievalConfig = (): RetrievalConfig => ({
|
||||
search_method: 'semantic_search' as RetrievalConfig['search_method'],
|
||||
reranking_enable: false,
|
||||
reranking_model: { reranking_provider_name: '', reranking_model_name: '' },
|
||||
top_k: 3,
|
||||
score_threshold_enabled: false,
|
||||
score_threshold: 0,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
indexType: IndexingType.QUALIFIED,
|
||||
hasSetIndexType: false,
|
||||
docForm: ChunkingMode.text,
|
||||
embeddingModel: createDefaultModel(),
|
||||
embeddingModelList: [],
|
||||
retrievalConfig: createRetrievalConfig(),
|
||||
showMultiModalTip: false,
|
||||
isModelAndRetrievalConfigDisabled: false,
|
||||
isQAConfirmDialogOpen: false,
|
||||
onIndexTypeChange: vi.fn(),
|
||||
onEmbeddingModelChange: vi.fn(),
|
||||
onRetrievalConfigChange: vi.fn(),
|
||||
onQAConfirmDialogClose: vi.fn(),
|
||||
onQAConfirmDialogConfirm: vi.fn(),
|
||||
}
|
||||
|
||||
describe('IndexingModeSection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render index mode title', () => {
|
||||
render(<IndexingModeSection {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.indexMode`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render qualified option when not locked to economical', () => {
|
||||
render(<IndexingModeSection {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.qualified`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render economical option when not locked to qualified', () => {
|
||||
render(<IndexingModeSection {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.economical`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should only show qualified option when hasSetIndexType and type is qualified', () => {
|
||||
render(<IndexingModeSection {...defaultProps} hasSetIndexType indexType={IndexingType.QUALIFIED} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.qualified`)).toBeInTheDocument()
|
||||
expect(screen.queryByText(`${ns}.stepTwo.economical`)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should only show economical option when hasSetIndexType and type is economical', () => {
|
||||
render(<IndexingModeSection {...defaultProps} hasSetIndexType indexType={IndexingType.ECONOMICAL} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.economical`)).toBeInTheDocument()
|
||||
expect(screen.queryByText(`${ns}.stepTwo.qualified`)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Embedding Model', () => {
|
||||
it('should show model selector when indexType is qualified', () => {
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.QUALIFIED} />)
|
||||
expect(screen.getByTestId('model-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show model selector when indexType is economical', () => {
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.ECONOMICAL} />)
|
||||
expect(screen.queryByTestId('model-selector')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should mark model selector as readonly when disabled', () => {
|
||||
render(<IndexingModeSection {...defaultProps} isModelAndRetrievalConfigDisabled />)
|
||||
expect(screen.getByTestId('model-selector')).toHaveAttribute('data-readonly', 'true')
|
||||
})
|
||||
|
||||
it('should call onEmbeddingModelChange when model selected', () => {
|
||||
const onEmbeddingModelChange = vi.fn()
|
||||
render(<IndexingModeSection {...defaultProps} onEmbeddingModelChange={onEmbeddingModelChange} />)
|
||||
fireEvent.click(screen.getByText('Select Model'))
|
||||
expect(onEmbeddingModelChange).toHaveBeenCalledWith({ provider: 'openai', model: 'text-embedding-3-small' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Retrieval Config', () => {
|
||||
it('should show RetrievalMethodConfig when qualified', () => {
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.QUALIFIED} />)
|
||||
expect(screen.getByTestId('retrieval-method-config')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show EconomicalRetrievalMethodConfig when economical', () => {
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.ECONOMICAL} />)
|
||||
expect(screen.getByTestId('economical-retrieval-config')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onRetrievalConfigChange from qualified config', () => {
|
||||
const onRetrievalConfigChange = vi.fn()
|
||||
render(<IndexingModeSection {...defaultProps} onRetrievalConfigChange={onRetrievalConfigChange} />)
|
||||
fireEvent.click(screen.getByText('Change Retrieval'))
|
||||
expect(onRetrievalConfigChange).toHaveBeenCalledWith({ search_method: 'updated' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Index Type Switching', () => {
|
||||
it('should call onIndexTypeChange when switching to qualified', () => {
|
||||
const onIndexTypeChange = vi.fn()
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.ECONOMICAL} onIndexTypeChange={onIndexTypeChange} />)
|
||||
const qualifiedCard = screen.getByText(`${ns}.stepTwo.qualified`).closest('[class*="rounded-xl"]')!
|
||||
fireEvent.click(qualifiedCard)
|
||||
expect(onIndexTypeChange).toHaveBeenCalledWith(IndexingType.QUALIFIED)
|
||||
})
|
||||
|
||||
it('should disable economical when docForm is QA', () => {
|
||||
render(<IndexingModeSection {...defaultProps} docForm={ChunkingMode.qa} />)
|
||||
// The economical option card should have disabled styling
|
||||
const economicalText = screen.getByText(`${ns}.stepTwo.economical`)
|
||||
const card = economicalText.closest('[class*="rounded-xl"]')
|
||||
expect(card).toHaveClass('pointer-events-none')
|
||||
})
|
||||
})
|
||||
|
||||
describe('High Quality Tip', () => {
|
||||
it('should show high quality tip when qualified is selected and not locked', () => {
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.QUALIFIED} hasSetIndexType={false} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.highQualityTip`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show high quality tip when index type is locked', () => {
|
||||
render(<IndexingModeSection {...defaultProps} indexType={IndexingType.QUALIFIED} hasSetIndexType />)
|
||||
expect(screen.queryByText(`${ns}.stepTwo.highQualityTip`)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('QA Confirm Dialog', () => {
|
||||
it('should call onQAConfirmDialogClose when cancel clicked', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<IndexingModeSection {...defaultProps} isQAConfirmDialogOpen onQAConfirmDialogClose={onClose} />)
|
||||
const cancelBtns = screen.getAllByText(`${ns}.stepTwo.cancel`)
|
||||
fireEvent.click(cancelBtns[0])
|
||||
expect(onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onQAConfirmDialogConfirm when confirm clicked', () => {
|
||||
const onConfirm = vi.fn()
|
||||
render(<IndexingModeSection {...defaultProps} isQAConfirmDialogOpen onQAConfirmDialogConfirm={onConfirm} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.switch`))
|
||||
expect(onConfirm).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dataset Settings Link', () => {
|
||||
it('should show settings link when economical and hasSetIndexType', () => {
|
||||
render(<IndexingModeSection {...defaultProps} hasSetIndexType indexType={IndexingType.ECONOMICAL} datasetId="ds-123" />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.datasetSettingLink`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.datasetSettingLink`).closest('a')).toHaveAttribute('href', '/datasets/ds-123/settings')
|
||||
})
|
||||
|
||||
it('should show settings link under model selector when disabled', () => {
|
||||
render(<IndexingModeSection {...defaultProps} isModelAndRetrievalConfigDisabled datasetId="ds-456" />)
|
||||
const links = screen.getAllByText(`${ns}.stepTwo.datasetSettingLink`)
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,92 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DelimiterInput, MaxLengthInput, OverlapInput } from '../inputs'
|
||||
|
||||
// i18n mock returns namespaced keys like "datasetCreation.stepTwo.separator"
|
||||
const ns = 'datasetCreation'
|
||||
|
||||
describe('DelimiterInput', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render separator label', () => {
|
||||
render(<DelimiterInput />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.separator`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render text input with placeholder', () => {
|
||||
render(<DelimiterInput />)
|
||||
const input = screen.getByPlaceholderText(`${ns}.stepTwo.separatorPlaceholder`)
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveAttribute('type', 'text')
|
||||
})
|
||||
|
||||
it('should pass through value and onChange props', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<DelimiterInput value="test-val" onChange={onChange} />)
|
||||
expect(screen.getByDisplayValue('test-val')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip content', () => {
|
||||
render(<DelimiterInput />)
|
||||
// Tooltip triggers render; component mounts without error
|
||||
expect(screen.getByText(`${ns}.stepTwo.separator`)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('MaxLengthInput', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render max length label', () => {
|
||||
render(<MaxLengthInput onChange={vi.fn()} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.maxLength`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render number input', () => {
|
||||
render(<MaxLengthInput onChange={vi.fn()} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept value prop', () => {
|
||||
render(<MaxLengthInput value={500} onChange={vi.fn()} />)
|
||||
expect(screen.getByDisplayValue('500')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have min of 1', () => {
|
||||
render(<MaxLengthInput onChange={vi.fn()} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toHaveAttribute('min', '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('OverlapInput', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render overlap label', () => {
|
||||
render(<OverlapInput onChange={vi.fn()} />)
|
||||
expect(screen.getAllByText(new RegExp(`${ns}.stepTwo.overlap`)).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render number input', () => {
|
||||
render(<OverlapInput onChange={vi.fn()} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept value prop', () => {
|
||||
render(<OverlapInput value={50} onChange={vi.fn()} />)
|
||||
expect(screen.getByDisplayValue('50')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have min of 1', () => {
|
||||
render(<OverlapInput onChange={vi.fn()} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toHaveAttribute('min', '1')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,160 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OptionCard, OptionCardHeader } from '../option-card'
|
||||
|
||||
// Override global next/image auto-mock: tests assert on rendered <img> elements
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ src, alt, ...props }: { src?: string, alt?: string, width?: number, height?: number }) => (
|
||||
<img src={src} alt={alt} {...props} />
|
||||
),
|
||||
}))
|
||||
|
||||
describe('OptionCardHeader', () => {
|
||||
const defaultProps = {
|
||||
icon: <span data-testid="icon">icon</span>,
|
||||
title: <span>Test Title</span>,
|
||||
description: 'Test description',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render icon, title and description', () => {
|
||||
render(<OptionCardHeader {...defaultProps} />)
|
||||
expect(screen.getByTestId('icon')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show effect image when active and effectImg provided', () => {
|
||||
const { container } = render(
|
||||
<OptionCardHeader {...defaultProps} isActive effectImg="/effect.png" />,
|
||||
)
|
||||
const img = container.querySelector('img')
|
||||
expect(img).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show effect image when not active', () => {
|
||||
const { container } = render(
|
||||
<OptionCardHeader {...defaultProps} isActive={false} effectImg="/effect.png" />,
|
||||
)
|
||||
expect(container.querySelector('img')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply cursor-pointer when not disabled', () => {
|
||||
const { container } = render(<OptionCardHeader {...defaultProps} />)
|
||||
expect(container.firstChild).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('should not apply cursor-pointer when disabled', () => {
|
||||
const { container } = render(<OptionCardHeader {...defaultProps} disabled />)
|
||||
expect(container.firstChild).not.toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('should apply activeClassName when active', () => {
|
||||
const { container } = render(
|
||||
<OptionCardHeader {...defaultProps} isActive activeClassName="custom-active" />,
|
||||
)
|
||||
expect(container.firstChild).toHaveClass('custom-active')
|
||||
})
|
||||
|
||||
it('should not apply activeClassName when not active', () => {
|
||||
const { container } = render(
|
||||
<OptionCardHeader {...defaultProps} isActive={false} activeClassName="custom-active" />,
|
||||
)
|
||||
expect(container.firstChild).not.toHaveClass('custom-active')
|
||||
})
|
||||
})
|
||||
|
||||
describe('OptionCard', () => {
|
||||
const defaultProps = {
|
||||
icon: <span data-testid="icon">icon</span>,
|
||||
title: <span>Card Title</span> as React.ReactNode,
|
||||
description: 'Card description',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render header content', () => {
|
||||
render(<OptionCard {...defaultProps} />)
|
||||
expect(screen.getByText('Card Title')).toBeInTheDocument()
|
||||
expect(screen.getByText('Card description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onSwitched when clicked while not active and not disabled', () => {
|
||||
const onSwitched = vi.fn()
|
||||
const { container } = render(
|
||||
<OptionCard {...defaultProps} isActive={false} onSwitched={onSwitched} />,
|
||||
)
|
||||
fireEvent.click(container.firstChild!)
|
||||
expect(onSwitched).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should not call onSwitched when already active', () => {
|
||||
const onSwitched = vi.fn()
|
||||
const { container } = render(
|
||||
<OptionCard {...defaultProps} isActive onSwitched={onSwitched} />,
|
||||
)
|
||||
fireEvent.click(container.firstChild!)
|
||||
expect(onSwitched).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onSwitched when disabled', () => {
|
||||
const onSwitched = vi.fn()
|
||||
const { container } = render(
|
||||
<OptionCard {...defaultProps} disabled onSwitched={onSwitched} />,
|
||||
)
|
||||
fireEvent.click(container.firstChild!)
|
||||
expect(onSwitched).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show children and actions when active', () => {
|
||||
render(
|
||||
<OptionCard {...defaultProps} isActive actions={<button>Action</button>}>
|
||||
<div>Body Content</div>
|
||||
</OptionCard>,
|
||||
)
|
||||
expect(screen.getByText('Body Content')).toBeInTheDocument()
|
||||
expect(screen.getByText('Action')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show children when not active', () => {
|
||||
render(
|
||||
<OptionCard {...defaultProps} isActive={false}>
|
||||
<div>Body Content</div>
|
||||
</OptionCard>,
|
||||
)
|
||||
expect(screen.queryByText('Body Content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply selected border style when active and not noHighlight', () => {
|
||||
const { container } = render(<OptionCard {...defaultProps} isActive />)
|
||||
expect(container.firstChild).toHaveClass('border-components-option-card-option-selected-border')
|
||||
})
|
||||
|
||||
it('should not apply selected border when noHighlight is true', () => {
|
||||
const { container } = render(<OptionCard {...defaultProps} isActive noHighlight />)
|
||||
expect(container.firstChild).not.toHaveClass('border-components-option-card-option-selected-border')
|
||||
})
|
||||
|
||||
it('should apply disabled opacity and pointer-events styles', () => {
|
||||
const { container } = render(<OptionCard {...defaultProps} disabled />)
|
||||
expect(container.firstChild).toHaveClass('pointer-events-none')
|
||||
expect(container.firstChild).toHaveClass('opacity-50')
|
||||
})
|
||||
|
||||
it('should forward custom className', () => {
|
||||
const { container } = render(<OptionCard {...defaultProps} className="custom-class" />)
|
||||
expect(container.firstChild).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should forward custom style', () => {
|
||||
const { container } = render(
|
||||
<OptionCard {...defaultProps} style={{ maxWidth: '300px' }} />,
|
||||
)
|
||||
expect((container.firstChild as HTMLElement).style.maxWidth).toBe('300px')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,150 @@
|
||||
import type { ParentChildConfig } from '../../hooks'
|
||||
import type { PreProcessingRule } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { ParentChildOptions } from '../parent-child-options'
|
||||
|
||||
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
||||
default: ({ onSummaryIndexSettingChange }: { onSummaryIndexSettingChange?: (val: Record<string, unknown>) => void }) => (
|
||||
<div data-testid="summary-index-setting">
|
||||
<button data-testid="summary-toggle" onClick={() => onSummaryIndexSettingChange?.({ enable: true })}>Toggle</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
IS_CE_EDITION: true,
|
||||
}))
|
||||
|
||||
const ns = 'datasetCreation'
|
||||
|
||||
const createRules = (): PreProcessingRule[] => [
|
||||
{ id: 'remove_extra_spaces', enabled: true },
|
||||
{ id: 'remove_urls_emails', enabled: false },
|
||||
]
|
||||
|
||||
const createParentChildConfig = (overrides?: Partial<ParentChildConfig>): ParentChildConfig => ({
|
||||
chunkForContext: 'paragraph',
|
||||
parent: { delimiter: '\\n\\n', maxLength: 2000 },
|
||||
child: { delimiter: '\\n', maxLength: 500 },
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
parentChildConfig: createParentChildConfig(),
|
||||
rules: createRules(),
|
||||
currentDocForm: ChunkingMode.parentChild,
|
||||
isActive: true,
|
||||
isInUpload: false,
|
||||
isNotUploadInEmptyDataset: false,
|
||||
onDocFormChange: vi.fn(),
|
||||
onChunkForContextChange: vi.fn(),
|
||||
onParentDelimiterChange: vi.fn(),
|
||||
onParentMaxLengthChange: vi.fn(),
|
||||
onChildDelimiterChange: vi.fn(),
|
||||
onChildMaxLengthChange: vi.fn(),
|
||||
onRuleToggle: vi.fn(),
|
||||
onPreview: vi.fn(),
|
||||
onReset: vi.fn(),
|
||||
}
|
||||
|
||||
describe('ParentChildOptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render parent-child title', () => {
|
||||
render(<ParentChildOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.parentChild`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render parent chunk context section when active', () => {
|
||||
render(<ParentChildOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.parentChunkForContext`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.paragraph`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.fullDoc`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render child chunk retrieval section when active', () => {
|
||||
render(<ParentChildOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.childChunkForRetrieval`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render rules section when active', () => {
|
||||
render(<ParentChildOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.removeExtraSpaces`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.removeUrlEmails`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render preview and reset buttons when active', () => {
|
||||
render(<ParentChildOptions {...defaultProps} />)
|
||||
expect(screen.getByText(`${ns}.stepTwo.previewChunk`)).toBeInTheDocument()
|
||||
expect(screen.getByText(`${ns}.stepTwo.reset`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render body when not active', () => {
|
||||
render(<ParentChildOptions {...defaultProps} isActive={false} />)
|
||||
expect(screen.queryByText(`${ns}.stepTwo.parentChunkForContext`)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onPreview when preview button clicked', () => {
|
||||
const onPreview = vi.fn()
|
||||
render(<ParentChildOptions {...defaultProps} onPreview={onPreview} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.previewChunk`))
|
||||
expect(onPreview).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onReset when reset button clicked', () => {
|
||||
const onReset = vi.fn()
|
||||
render(<ParentChildOptions {...defaultProps} onReset={onReset} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.reset`))
|
||||
expect(onReset).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onRuleToggle when rule clicked', () => {
|
||||
const onRuleToggle = vi.fn()
|
||||
render(<ParentChildOptions {...defaultProps} onRuleToggle={onRuleToggle} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.removeUrlEmails`))
|
||||
expect(onRuleToggle).toHaveBeenCalledWith('remove_urls_emails')
|
||||
})
|
||||
|
||||
it('should call onDocFormChange with parentChild when card switched', () => {
|
||||
const onDocFormChange = vi.fn()
|
||||
render(<ParentChildOptions {...defaultProps} isActive={false} onDocFormChange={onDocFormChange} />)
|
||||
const titleEl = screen.getByText(`${ns}.stepTwo.parentChild`)
|
||||
fireEvent.click(titleEl.closest('[class*="rounded-xl"]')!)
|
||||
expect(onDocFormChange).toHaveBeenCalledWith(ChunkingMode.parentChild)
|
||||
})
|
||||
|
||||
it('should call onChunkForContextChange when full-doc chosen', () => {
|
||||
const onChunkForContextChange = vi.fn()
|
||||
render(<ParentChildOptions {...defaultProps} onChunkForContextChange={onChunkForContextChange} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.fullDoc`))
|
||||
expect(onChunkForContextChange).toHaveBeenCalledWith('full-doc')
|
||||
})
|
||||
|
||||
it('should call onChunkForContextChange when paragraph chosen', () => {
|
||||
const onChunkForContextChange = vi.fn()
|
||||
const config = createParentChildConfig({ chunkForContext: 'full-doc' })
|
||||
render(<ParentChildOptions {...defaultProps} parentChildConfig={config} onChunkForContextChange={onChunkForContextChange} />)
|
||||
fireEvent.click(screen.getByText(`${ns}.stepTwo.paragraph`))
|
||||
expect(onChunkForContextChange).toHaveBeenCalledWith('paragraph')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Summary Index Setting', () => {
|
||||
it('should render SummaryIndexSetting when showSummaryIndexSetting is true', () => {
|
||||
render(<ParentChildOptions {...defaultProps} showSummaryIndexSetting />)
|
||||
expect(screen.getByTestId('summary-index-setting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render SummaryIndexSetting when showSummaryIndexSetting is false', () => {
|
||||
render(<ParentChildOptions {...defaultProps} showSummaryIndexSetting={false} />)
|
||||
expect(screen.queryByTestId('summary-index-setting')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,166 @@
|
||||
import type { ParentChildConfig } from '../../hooks'
|
||||
import type { FileIndexingEstimateResponse } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode, DataSourceType } from '@/models/datasets'
|
||||
import { PreviewPanel } from '../preview-panel'
|
||||
|
||||
vi.mock('@/app/components/base/float-right-container', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div data-testid="float-container">{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/badge', () => ({
|
||||
default: ({ text }: { text: string }) => <span data-testid="badge">{text}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/skeleton', () => ({
|
||||
SkeletonContainer: ({ children }: { children: React.ReactNode }) => <div data-testid="skeleton">{children}</div>,
|
||||
SkeletonPoint: () => <span />,
|
||||
SkeletonRectangle: () => <span />,
|
||||
SkeletonRow: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../chunk', () => ({
|
||||
ChunkContainer: ({ children, label }: { children: React.ReactNode, label: string }) => (
|
||||
<div data-testid="chunk-container">
|
||||
{label}
|
||||
:
|
||||
{' '}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
QAPreview: ({ qa }: { qa: { question: string } }) => <div data-testid="qa-preview">{qa.question}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../common/document-picker/preview-document-picker', () => ({
|
||||
default: () => <div data-testid="doc-picker" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../documents/detail/completed/common/summary-label', () => ({
|
||||
default: ({ summary }: { summary: string }) => <span data-testid="summary">{summary}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../formatted-text/flavours/preview-slice', () => ({
|
||||
PreviewSlice: ({ label, text }: { label: string, text: string }) => (
|
||||
<span data-testid="preview-slice">
|
||||
{label}
|
||||
:
|
||||
{' '}
|
||||
{text}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../formatted-text/formatted', () => ({
|
||||
FormattedText: ({ children }: { children: React.ReactNode }) => <p data-testid="formatted-text">{children}</p>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../preview/container', () => ({
|
||||
default: ({ children, header }: { children: React.ReactNode, header: React.ReactNode }) => (
|
||||
<div data-testid="preview-container">
|
||||
{header}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../preview/header', () => ({
|
||||
PreviewHeader: ({ children, title }: { children: React.ReactNode, title: string }) => (
|
||||
<div data-testid="preview-header">
|
||||
{title}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
FULL_DOC_PREVIEW_LENGTH: 3,
|
||||
}))
|
||||
|
||||
describe('PreviewPanel', () => {
|
||||
const defaultProps = {
|
||||
isMobile: false,
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
currentDocForm: ChunkingMode.text,
|
||||
parentChildConfig: { chunkForContext: 'paragraph' } as ParentChildConfig,
|
||||
pickerFiles: [{ id: '1', name: 'file.pdf', extension: 'pdf' }],
|
||||
pickerValue: { id: '1', name: 'file.pdf', extension: 'pdf' },
|
||||
isIdle: false,
|
||||
isPending: false,
|
||||
onPickerChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render preview header with title', () => {
|
||||
render(<PreviewPanel {...defaultProps} />)
|
||||
expect(screen.getByTestId('preview-header')).toHaveTextContent('datasetCreation.stepTwo.preview')
|
||||
})
|
||||
|
||||
it('should render document picker', () => {
|
||||
render(<PreviewPanel {...defaultProps} />)
|
||||
expect(screen.getByTestId('doc-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show idle state when isIdle is true', () => {
|
||||
render(<PreviewPanel {...defaultProps} isIdle={true} />)
|
||||
expect(screen.getByText('datasetCreation.stepTwo.previewChunkTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading skeletons when isPending', () => {
|
||||
render(<PreviewPanel {...defaultProps} isPending={true} />)
|
||||
expect(screen.getAllByTestId('skeleton')).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('should render text preview chunks', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = {
|
||||
total_segments: 2,
|
||||
preview: [
|
||||
{ content: 'chunk 1 text', child_chunks: [], summary: '' },
|
||||
{ content: 'chunk 2 text', child_chunks: [], summary: 'summary text' },
|
||||
],
|
||||
}
|
||||
render(<PreviewPanel {...defaultProps} estimate={estimate as FileIndexingEstimateResponse} />)
|
||||
expect(screen.getAllByTestId('chunk-container')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render QA preview', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = {
|
||||
qa_preview: [
|
||||
{ question: 'Q1', answer: 'A1' },
|
||||
],
|
||||
}
|
||||
render(
|
||||
<PreviewPanel
|
||||
{...defaultProps}
|
||||
currentDocForm={ChunkingMode.qa}
|
||||
estimate={estimate as FileIndexingEstimateResponse}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('qa-preview')).toHaveTextContent('Q1')
|
||||
})
|
||||
|
||||
it('should render parent-child preview', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = {
|
||||
preview: [
|
||||
{ content: 'parent chunk', child_chunks: ['child1', 'child2'], summary: '' },
|
||||
],
|
||||
}
|
||||
render(
|
||||
<PreviewPanel
|
||||
{...defaultProps}
|
||||
currentDocForm={ChunkingMode.parentChild}
|
||||
estimate={estimate as FileIndexingEstimateResponse}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getAllByTestId('preview-slice')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should show badge with chunk count for non-QA mode', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = { total_segments: 5, preview: [] }
|
||||
render(<PreviewPanel {...defaultProps} estimate={estimate as FileIndexingEstimateResponse} />)
|
||||
expect(screen.getByTestId('badge')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,46 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { StepTwoFooter } from '../step-two-footer'
|
||||
|
||||
describe('StepTwoFooter', () => {
|
||||
const defaultProps = {
|
||||
isCreating: false,
|
||||
onPrevious: vi.fn(),
|
||||
onCreate: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render previous and next buttons when not isSetting', () => {
|
||||
render(<StepTwoFooter {...defaultProps} />)
|
||||
expect(screen.getByText('datasetCreation.stepTwo.previousStep')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepTwo.nextStep')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render save and cancel buttons when isSetting', () => {
|
||||
render(<StepTwoFooter {...defaultProps} isSetting />)
|
||||
expect(screen.getByText('datasetCreation.stepTwo.save')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepTwo.cancel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onPrevious on previous button click', () => {
|
||||
render(<StepTwoFooter {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepTwo.previousStep'))
|
||||
expect(defaultProps.onPrevious).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onCreate on next button click', () => {
|
||||
render(<StepTwoFooter {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepTwo.nextStep'))
|
||||
expect(defaultProps.onCreate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call onCancel on cancel button click in settings mode', () => {
|
||||
render(<StepTwoFooter {...defaultProps} isSetting />)
|
||||
fireEvent.click(screen.getByText('datasetCreation.stepTwo.cancel'))
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import escape from '../escape'
|
||||
|
||||
describe('escape', () => {
|
||||
// Basic special character escaping
|
||||
it('should escape null character', () => {
|
||||
expect(escape('\0')).toBe('\\0')
|
||||
})
|
||||
|
||||
it('should escape backspace', () => {
|
||||
expect(escape('\b')).toBe('\\b')
|
||||
})
|
||||
|
||||
it('should escape form feed', () => {
|
||||
expect(escape('\f')).toBe('\\f')
|
||||
})
|
||||
|
||||
it('should escape newline', () => {
|
||||
expect(escape('\n')).toBe('\\n')
|
||||
})
|
||||
|
||||
it('should escape carriage return', () => {
|
||||
expect(escape('\r')).toBe('\\r')
|
||||
})
|
||||
|
||||
it('should escape tab', () => {
|
||||
expect(escape('\t')).toBe('\\t')
|
||||
})
|
||||
|
||||
it('should escape vertical tab', () => {
|
||||
expect(escape('\v')).toBe('\\v')
|
||||
})
|
||||
|
||||
it('should escape single quote', () => {
|
||||
expect(escape('\'')).toBe('\\\'')
|
||||
})
|
||||
|
||||
// Multiple special characters in one string
|
||||
it('should escape multiple special characters', () => {
|
||||
expect(escape('line1\nline2\ttab')).toBe('line1\\nline2\\ttab')
|
||||
})
|
||||
|
||||
it('should escape mixed special characters', () => {
|
||||
expect(escape('\n\r\t')).toBe('\\n\\r\\t')
|
||||
})
|
||||
|
||||
it('should return empty string for null input', () => {
|
||||
expect(escape(null as unknown as string)).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for undefined input', () => {
|
||||
expect(escape(undefined as unknown as string)).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for empty string input', () => {
|
||||
expect(escape('')).toBe('')
|
||||
})
|
||||
|
||||
it('should return empty string for non-string input', () => {
|
||||
expect(escape(123 as unknown as string)).toBe('')
|
||||
})
|
||||
|
||||
// Pass-through for normal strings
|
||||
it('should leave normal text unchanged', () => {
|
||||
expect(escape('hello world')).toBe('hello world')
|
||||
})
|
||||
|
||||
it('should leave special regex characters unchanged', () => {
|
||||
expect(escape('a.b*c+d')).toBe('a.b*c+d')
|
||||
})
|
||||
|
||||
it('should handle strings with no special characters', () => {
|
||||
expect(escape('abc123')).toBe('abc123')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import unescape from '../unescape'
|
||||
|
||||
describe('unescape', () => {
|
||||
// Basic escape sequences
|
||||
it('should unescape \\n to newline', () => {
|
||||
expect(unescape('\\n')).toBe('\n')
|
||||
})
|
||||
|
||||
it('should unescape \\t to tab', () => {
|
||||
expect(unescape('\\t')).toBe('\t')
|
||||
})
|
||||
|
||||
it('should unescape \\r to carriage return', () => {
|
||||
expect(unescape('\\r')).toBe('\r')
|
||||
})
|
||||
|
||||
it('should unescape \\b to backspace', () => {
|
||||
expect(unescape('\\b')).toBe('\b')
|
||||
})
|
||||
|
||||
it('should unescape \\f to form feed', () => {
|
||||
expect(unescape('\\f')).toBe('\f')
|
||||
})
|
||||
|
||||
it('should unescape \\v to vertical tab', () => {
|
||||
expect(unescape('\\v')).toBe('\v')
|
||||
})
|
||||
|
||||
it('should unescape \\0 to null character', () => {
|
||||
expect(unescape('\\0')).toBe('\0')
|
||||
})
|
||||
|
||||
it('should unescape \\\\ to backslash', () => {
|
||||
expect(unescape('\\\\')).toBe('\\')
|
||||
})
|
||||
|
||||
it('should unescape \\\' to single quote', () => {
|
||||
expect(unescape('\\\'')).toBe('\'')
|
||||
})
|
||||
|
||||
it('should unescape \\" to double quote', () => {
|
||||
expect(unescape('\\"')).toBe('"')
|
||||
})
|
||||
|
||||
// Hex escape sequences (\\xNN)
|
||||
it('should unescape 2-digit hex sequences', () => {
|
||||
expect(unescape('\\x41')).toBe('A')
|
||||
expect(unescape('\\x61')).toBe('a')
|
||||
})
|
||||
|
||||
// Unicode escape sequences (\\uNNNN)
|
||||
it('should unescape 4-digit unicode sequences', () => {
|
||||
expect(unescape('\\u0041')).toBe('A')
|
||||
expect(unescape('\\u4e2d')).toBe('中')
|
||||
})
|
||||
|
||||
// Variable-length unicode (\\u{NNNN})
|
||||
it('should unescape variable-length unicode sequences', () => {
|
||||
expect(unescape('\\u{41}')).toBe('A')
|
||||
expect(unescape('\\u{1F600}')).toBe('😀')
|
||||
})
|
||||
|
||||
// Octal escape sequences
|
||||
it('should unescape octal sequences', () => {
|
||||
expect(unescape('\\101')).toBe('A') // 0o101 = 65 = 'A'
|
||||
expect(unescape('\\12')).toBe('\n') // 0o12 = 10 = '\n'
|
||||
})
|
||||
|
||||
// Python-style 8-digit unicode (\\UNNNNNNNN)
|
||||
it('should unescape Python-style 8-digit unicode', () => {
|
||||
expect(unescape('\\U0001F3B5')).toBe('🎵')
|
||||
})
|
||||
|
||||
// Multiple escape sequences
|
||||
it('should unescape multiple sequences in one string', () => {
|
||||
expect(unescape('line1\\nline2\\ttab')).toBe('line1\nline2\ttab')
|
||||
})
|
||||
|
||||
// Mixed content
|
||||
it('should leave non-escape content unchanged', () => {
|
||||
expect(unescape('hello world')).toBe('hello world')
|
||||
})
|
||||
|
||||
it('should handle mixed escaped and non-escaped content', () => {
|
||||
expect(unescape('before\\nafter')).toBe('before\nafter')
|
||||
})
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(unescape('')).toBe('')
|
||||
})
|
||||
|
||||
it('should handle string with no escape sequences', () => {
|
||||
expect(unescape('abc123')).toBe('abc123')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,186 @@
|
||||
import type { CustomFile, FullDocumentDetail, ProcessRule } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode, DataSourceType } from '@/models/datasets'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
|
||||
// Hoisted mocks
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toastNotify: vi.fn(),
|
||||
mutateAsync: vi.fn(),
|
||||
isReRankModelSelected: vi.fn(() => true),
|
||||
trackEvent: vi.fn(),
|
||||
invalidDatasetList: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: mocks.toastNotify },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: mocks.trackEvent,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({
|
||||
isReRankModelSelected: mocks.isReRankModelSelected,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-create-dataset', () => ({
|
||||
useCreateFirstDocument: () => ({ mutateAsync: mocks.mutateAsync, isPending: false }),
|
||||
useCreateDocument: () => ({ mutateAsync: mocks.mutateAsync, isPending: false }),
|
||||
getNotionInfo: vi.fn(() => []),
|
||||
getWebsiteInfo: vi.fn(() => ({})),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useInvalidDatasetList: () => mocks.invalidDatasetList,
|
||||
}))
|
||||
|
||||
const { useDocumentCreation } = await import('../use-document-creation')
|
||||
const { IndexingType } = await import('../use-indexing-config')
|
||||
|
||||
describe('useDocumentCreation', () => {
|
||||
const defaultOptions = {
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
files: [{ id: 'f-1', name: 'test.txt' }] as CustomFile[],
|
||||
notionPages: [],
|
||||
notionCredentialId: '',
|
||||
websitePages: [],
|
||||
}
|
||||
|
||||
const defaultValidationParams = {
|
||||
segmentationType: 'general',
|
||||
maxChunkLength: 1024,
|
||||
limitMaxChunkLength: 4000,
|
||||
overlap: 50,
|
||||
indexType: IndexingType.QUALIFIED,
|
||||
embeddingModel: { provider: 'openai', model: 'text-embedding-3-small' },
|
||||
rerankModelList: [],
|
||||
retrievalConfig: {
|
||||
search_method: RETRIEVE_METHOD.semantic,
|
||||
reranking_enable: false,
|
||||
reranking_model: { reranking_provider_name: '', reranking_model_name: '' },
|
||||
top_k: 3,
|
||||
score_threshold_enabled: false,
|
||||
score_threshold: 0.5,
|
||||
} as RetrievalConfig,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.isReRankModelSelected.mockReturnValue(true)
|
||||
})
|
||||
|
||||
describe('validateParams', () => {
|
||||
it('should return true for valid params', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
expect(result.current.validateParams(defaultValidationParams)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when overlap > maxChunkLength', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
const invalid = { ...defaultValidationParams, overlap: 2000, maxChunkLength: 1000 }
|
||||
expect(result.current.validateParams(invalid)).toBe(false)
|
||||
expect(mocks.toastNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should return false when maxChunkLength > limitMaxChunkLength', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
const invalid = { ...defaultValidationParams, maxChunkLength: 5000, limitMaxChunkLength: 4000 }
|
||||
expect(result.current.validateParams(invalid)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when qualified but no embedding model', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
const invalid = {
|
||||
...defaultValidationParams,
|
||||
indexType: IndexingType.QUALIFIED,
|
||||
embeddingModel: { provider: '', model: '' },
|
||||
}
|
||||
expect(result.current.validateParams(invalid)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when rerank model not selected', () => {
|
||||
mocks.isReRankModelSelected.mockReturnValue(false)
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
expect(result.current.validateParams(defaultValidationParams)).toBe(false)
|
||||
})
|
||||
|
||||
it('should skip embedding/rerank checks when isSetting is true', () => {
|
||||
mocks.isReRankModelSelected.mockReturnValue(false)
|
||||
const { result } = renderHook(() =>
|
||||
useDocumentCreation({ ...defaultOptions, isSetting: true }),
|
||||
)
|
||||
const params = {
|
||||
...defaultValidationParams,
|
||||
embeddingModel: { provider: '', model: '' },
|
||||
}
|
||||
expect(result.current.validateParams(params)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildCreationParams', () => {
|
||||
it('should build params for FILE data source', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
const processRule = { mode: 'custom', rules: {} } as unknown as ProcessRule
|
||||
const retrievalConfig = defaultValidationParams.retrievalConfig
|
||||
const embeddingModel = { provider: 'openai', model: 'text-embedding-3-small' }
|
||||
|
||||
const params = result.current.buildCreationParams(
|
||||
ChunkingMode.text,
|
||||
'English',
|
||||
processRule,
|
||||
retrievalConfig,
|
||||
embeddingModel,
|
||||
'high_quality',
|
||||
)
|
||||
|
||||
expect(params).not.toBeNull()
|
||||
expect(params!.data_source!.type).toBe(DataSourceType.FILE)
|
||||
expect(params!.data_source!.info_list.file_info_list?.file_ids).toContain('f-1')
|
||||
expect(params!.embedding_model).toBe('text-embedding-3-small')
|
||||
expect(params!.embedding_model_provider).toBe('openai')
|
||||
})
|
||||
|
||||
it('should build params for isSetting mode', () => {
|
||||
const detail = { id: 'doc-1' } as FullDocumentDetail
|
||||
const { result } = renderHook(() =>
|
||||
useDocumentCreation({ ...defaultOptions, isSetting: true, documentDetail: detail }),
|
||||
)
|
||||
const params = result.current.buildCreationParams(
|
||||
ChunkingMode.text,
|
||||
'English',
|
||||
{ mode: 'custom', rules: {} } as unknown as ProcessRule,
|
||||
defaultValidationParams.retrievalConfig,
|
||||
{ provider: 'openai', model: 'text-embedding-3-small' },
|
||||
'high_quality',
|
||||
)
|
||||
|
||||
expect(params!.original_document_id).toBe('doc-1')
|
||||
expect(params!.data_source).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validatePreviewParams', () => {
|
||||
it('should return true when maxChunkLength is within limit', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
expect(result.current.validatePreviewParams(1024)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when maxChunkLength exceeds limit', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
expect(result.current.validatePreviewParams(999999)).toBe(false)
|
||||
expect(mocks.toastNotify).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isCreating', () => {
|
||||
it('should reflect mutation pending state', () => {
|
||||
const { result } = renderHook(() => useDocumentCreation(defaultOptions))
|
||||
expect(result.current.isCreating).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,161 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
|
||||
// Hoisted mock state
|
||||
const mocks = vi.hoisted(() => ({
|
||||
rerankModelList: [] as Array<{ provider: { provider: string }, model: string }>,
|
||||
rerankDefaultModel: null as { provider: { provider: string }, model: string } | null,
|
||||
isRerankDefaultModelValid: null as { provider: { provider: string }, model: string } | null,
|
||||
embeddingModelList: [] as Array<{ provider: { provider: string }, model: string }>,
|
||||
defaultEmbeddingModel: null as { provider: { provider: string }, model: string } | null,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel: () => ({
|
||||
modelList: mocks.rerankModelList,
|
||||
defaultModel: mocks.rerankDefaultModel,
|
||||
currentModel: mocks.isRerankDefaultModelValid,
|
||||
}),
|
||||
useModelList: () => ({ data: mocks.embeddingModelList }),
|
||||
useDefaultModel: () => ({ data: mocks.defaultEmbeddingModel }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/settings/utils', () => ({
|
||||
checkShowMultiModalTip: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
const { IndexingType, useIndexingConfig } = await import('../use-indexing-config')
|
||||
|
||||
describe('useIndexingConfig', () => {
|
||||
const defaultOptions = {
|
||||
isAPIKeySet: true,
|
||||
hasSetIndexType: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.rerankModelList = []
|
||||
mocks.rerankDefaultModel = null
|
||||
mocks.isRerankDefaultModelValid = null
|
||||
mocks.embeddingModelList = []
|
||||
mocks.defaultEmbeddingModel = null
|
||||
})
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should default to QUALIFIED when API key is set', () => {
|
||||
const { result } = renderHook(() => useIndexingConfig(defaultOptions))
|
||||
expect(result.current.indexType).toBe(IndexingType.QUALIFIED)
|
||||
})
|
||||
|
||||
it('should default to ECONOMICAL when API key is not set', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIndexingConfig({ ...defaultOptions, isAPIKeySet: false }),
|
||||
)
|
||||
expect(result.current.indexType).toBe(IndexingType.ECONOMICAL)
|
||||
})
|
||||
|
||||
it('should use initial index type when provided', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIndexingConfig({
|
||||
...defaultOptions,
|
||||
initialIndexType: IndexingType.ECONOMICAL,
|
||||
}),
|
||||
)
|
||||
expect(result.current.indexType).toBe(IndexingType.ECONOMICAL)
|
||||
})
|
||||
|
||||
it('should use initial embedding model when provided', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIndexingConfig({
|
||||
...defaultOptions,
|
||||
initialEmbeddingModel: { provider: 'openai', model: 'text-embedding-3-small' },
|
||||
}),
|
||||
)
|
||||
expect(result.current.embeddingModel).toEqual({
|
||||
provider: 'openai',
|
||||
model: 'text-embedding-3-small',
|
||||
})
|
||||
})
|
||||
|
||||
it('should use initial retrieval config when provided', () => {
|
||||
const config = {
|
||||
search_method: RETRIEVE_METHOD.fullText,
|
||||
reranking_enable: false,
|
||||
reranking_model: { reranking_provider_name: '', reranking_model_name: '' },
|
||||
top_k: 5,
|
||||
score_threshold_enabled: true,
|
||||
score_threshold: 0.8,
|
||||
}
|
||||
const { result } = renderHook(() =>
|
||||
useIndexingConfig({ ...defaultOptions, initialRetrievalConfig: config }),
|
||||
)
|
||||
expect(result.current.retrievalConfig.search_method).toBe(RETRIEVE_METHOD.fullText)
|
||||
expect(result.current.retrievalConfig.top_k).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setters', () => {
|
||||
it('should update index type', () => {
|
||||
const { result } = renderHook(() => useIndexingConfig(defaultOptions))
|
||||
|
||||
act(() => {
|
||||
result.current.setIndexType(IndexingType.ECONOMICAL)
|
||||
})
|
||||
expect(result.current.indexType).toBe(IndexingType.ECONOMICAL)
|
||||
})
|
||||
|
||||
it('should update embedding model', () => {
|
||||
const { result } = renderHook(() => useIndexingConfig(defaultOptions))
|
||||
|
||||
act(() => {
|
||||
result.current.setEmbeddingModel({ provider: 'cohere', model: 'embed-v3' })
|
||||
})
|
||||
expect(result.current.embeddingModel).toEqual({ provider: 'cohere', model: 'embed-v3' })
|
||||
})
|
||||
|
||||
it('should update retrieval config', () => {
|
||||
const { result } = renderHook(() => useIndexingConfig(defaultOptions))
|
||||
const newConfig = {
|
||||
...result.current.retrievalConfig,
|
||||
top_k: 10,
|
||||
}
|
||||
|
||||
act(() => {
|
||||
result.current.setRetrievalConfig(newConfig)
|
||||
})
|
||||
expect(result.current.retrievalConfig.top_k).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getIndexingTechnique', () => {
|
||||
it('should return initialIndexType when provided', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIndexingConfig({
|
||||
...defaultOptions,
|
||||
initialIndexType: IndexingType.ECONOMICAL,
|
||||
}),
|
||||
)
|
||||
expect(result.current.getIndexingTechnique()).toBe(IndexingType.ECONOMICAL)
|
||||
})
|
||||
|
||||
it('should return current indexType when no initialIndexType', () => {
|
||||
const { result } = renderHook(() => useIndexingConfig(defaultOptions))
|
||||
expect(result.current.getIndexingTechnique()).toBe(IndexingType.QUALIFIED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should expose hasSetIndexType from options', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIndexingConfig({ ...defaultOptions, hasSetIndexType: true }),
|
||||
)
|
||||
expect(result.current.hasSetIndexType).toBe(true)
|
||||
})
|
||||
|
||||
it('should expose showMultiModalTip as boolean', () => {
|
||||
const { result } = renderHook(() => useIndexingConfig(defaultOptions))
|
||||
expect(typeof result.current.showMultiModalTip).toBe('boolean')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,127 @@
|
||||
import type { IndexingType } from '../use-indexing-config'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { ChunkingMode, CrawlResultItem, CustomFile, ProcessRule } from '@/models/datasets'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
|
||||
// Hoisted mocks
|
||||
const mocks = vi.hoisted(() => ({
|
||||
fileMutate: vi.fn(),
|
||||
fileReset: vi.fn(),
|
||||
notionMutate: vi.fn(),
|
||||
notionReset: vi.fn(),
|
||||
webMutate: vi.fn(),
|
||||
webReset: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/knowledge/use-create-dataset', () => ({
|
||||
useFetchFileIndexingEstimateForFile: () => ({
|
||||
mutate: mocks.fileMutate,
|
||||
reset: mocks.fileReset,
|
||||
data: { tokens: 100, total_segments: 5 },
|
||||
isIdle: true,
|
||||
isPending: false,
|
||||
}),
|
||||
useFetchFileIndexingEstimateForNotion: () => ({
|
||||
mutate: mocks.notionMutate,
|
||||
reset: mocks.notionReset,
|
||||
data: null,
|
||||
isIdle: true,
|
||||
isPending: false,
|
||||
}),
|
||||
useFetchFileIndexingEstimateForWeb: () => ({
|
||||
mutate: mocks.webMutate,
|
||||
reset: mocks.webReset,
|
||||
data: null,
|
||||
isIdle: true,
|
||||
isPending: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
const { useIndexingEstimate } = await import('../use-indexing-estimate')
|
||||
|
||||
describe('useIndexingEstimate', () => {
|
||||
const defaultOptions = {
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
currentDocForm: 'text_model' as ChunkingMode,
|
||||
docLanguage: 'English',
|
||||
files: [{ id: 'f-1', name: 'test.txt' }] as unknown as CustomFile[],
|
||||
previewNotionPage: {} as unknown as NotionPage,
|
||||
notionCredentialId: '',
|
||||
previewWebsitePage: {} as unknown as CrawlResultItem,
|
||||
indexingTechnique: 'high_quality' as unknown as IndexingType,
|
||||
processRule: { mode: 'custom', rules: {} } as unknown as ProcessRule,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('currentMutation selection', () => {
|
||||
it('should select file mutation for FILE type', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate(defaultOptions))
|
||||
expect(result.current.estimate).toEqual({ tokens: 100, total_segments: 5 })
|
||||
})
|
||||
|
||||
it('should select notion mutation for NOTION type', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate({
|
||||
...defaultOptions,
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
}))
|
||||
expect(result.current.estimate).toBeNull()
|
||||
})
|
||||
|
||||
it('should select web mutation for WEB type', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate({
|
||||
...defaultOptions,
|
||||
dataSourceType: DataSourceType.WEB,
|
||||
}))
|
||||
expect(result.current.estimate).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchEstimate', () => {
|
||||
it('should call file mutate for FILE type', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate(defaultOptions))
|
||||
result.current.fetchEstimate()
|
||||
expect(mocks.fileMutate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call notion mutate for NOTION type', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate({
|
||||
...defaultOptions,
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
}))
|
||||
result.current.fetchEstimate()
|
||||
expect(mocks.notionMutate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call web mutate for WEB type', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate({
|
||||
...defaultOptions,
|
||||
dataSourceType: DataSourceType.WEB,
|
||||
}))
|
||||
result.current.fetchEstimate()
|
||||
expect(mocks.webMutate).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('state properties', () => {
|
||||
it('should expose isIdle', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate(defaultOptions))
|
||||
expect(result.current.isIdle).toBe(true)
|
||||
})
|
||||
|
||||
it('should expose isPending', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate(defaultOptions))
|
||||
expect(result.current.isPending).toBe(false)
|
||||
})
|
||||
|
||||
it('should expose reset function', () => {
|
||||
const { result } = renderHook(() => useIndexingEstimate(defaultOptions))
|
||||
result.current.reset()
|
||||
expect(mocks.fileReset).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,198 @@
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem, CustomFile } from '@/models/datasets'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { usePreviewState } from '../use-preview-state'
|
||||
|
||||
// Factory functions
|
||||
const createFile = (id: string, name: string): CustomFile => ({
|
||||
id,
|
||||
name,
|
||||
size: 1024,
|
||||
type: 'text/plain',
|
||||
extension: 'txt',
|
||||
created_by: 'user',
|
||||
created_at: Date.now(),
|
||||
} as unknown as CustomFile)
|
||||
|
||||
const createNotionPage = (pageId: string, pageName: string): NotionPage => ({
|
||||
page_id: pageId,
|
||||
page_name: pageName,
|
||||
page_icon: null,
|
||||
parent_id: '',
|
||||
type: 'page',
|
||||
is_bound: true,
|
||||
} as unknown as NotionPage)
|
||||
|
||||
const createWebsitePage = (url: string, title: string): CrawlResultItem => ({
|
||||
source_url: url,
|
||||
title,
|
||||
markdown: '',
|
||||
description: '',
|
||||
} as unknown as CrawlResultItem)
|
||||
|
||||
describe('usePreviewState', () => {
|
||||
const files = [createFile('f-1', 'file1.txt'), createFile('f-2', 'file2.txt')]
|
||||
const notionPages = [createNotionPage('np-1', 'Page 1'), createNotionPage('np-2', 'Page 2')]
|
||||
const websitePages = [createWebsitePage('https://a.com', 'Site A'), createWebsitePage('https://b.com', 'Site B')]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('initial state for FILE', () => {
|
||||
it('should set first file as preview', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
files,
|
||||
notionPages: [],
|
||||
websitePages: [],
|
||||
}))
|
||||
expect(result.current.previewFile).toBe(files[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('initial state for NOTION', () => {
|
||||
it('should set first notion page as preview', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
files: [],
|
||||
notionPages,
|
||||
websitePages: [],
|
||||
}))
|
||||
expect(result.current.previewNotionPage).toBe(notionPages[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('initial state for WEB', () => {
|
||||
it('should set first website page as preview', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.WEB,
|
||||
files: [],
|
||||
notionPages: [],
|
||||
websitePages,
|
||||
}))
|
||||
expect(result.current.previewWebsitePage).toBe(websitePages[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPreviewPickerItems', () => {
|
||||
it('should return files for FILE type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
files,
|
||||
notionPages: [],
|
||||
websitePages: [],
|
||||
}))
|
||||
const items = result.current.getPreviewPickerItems()
|
||||
expect(items).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should return mapped notion pages for NOTION type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
files: [],
|
||||
notionPages,
|
||||
websitePages: [],
|
||||
}))
|
||||
const items = result.current.getPreviewPickerItems()
|
||||
expect(items).toHaveLength(2)
|
||||
expect(items[0]).toEqual({ id: 'np-1', name: 'Page 1', extension: 'md' })
|
||||
})
|
||||
|
||||
it('should return mapped website pages for WEB type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.WEB,
|
||||
files: [],
|
||||
notionPages: [],
|
||||
websitePages,
|
||||
}))
|
||||
const items = result.current.getPreviewPickerItems()
|
||||
expect(items).toHaveLength(2)
|
||||
expect(items[0]).toEqual({ id: 'https://a.com', name: 'Site A', extension: 'md' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPreviewPickerValue', () => {
|
||||
it('should return current preview file for FILE type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
files,
|
||||
notionPages: [],
|
||||
websitePages: [],
|
||||
}))
|
||||
const value = result.current.getPreviewPickerValue()
|
||||
expect(value).toBe(files[0])
|
||||
})
|
||||
|
||||
it('should return mapped notion page value for NOTION type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
files: [],
|
||||
notionPages,
|
||||
websitePages: [],
|
||||
}))
|
||||
const value = result.current.getPreviewPickerValue()
|
||||
expect(value).toEqual({ id: 'np-1', name: 'Page 1', extension: 'md' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('handlePreviewChange', () => {
|
||||
it('should change preview file for FILE type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
files,
|
||||
notionPages: [],
|
||||
websitePages: [],
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.handlePreviewChange({ id: 'f-2', name: 'file2.txt' })
|
||||
})
|
||||
expect(result.current.previewFile).toEqual({ id: 'f-2', name: 'file2.txt' })
|
||||
})
|
||||
|
||||
it('should change preview notion page for NOTION type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
files: [],
|
||||
notionPages,
|
||||
websitePages: [],
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.handlePreviewChange({ id: 'np-2', name: 'Page 2' })
|
||||
})
|
||||
expect(result.current.previewNotionPage).toBe(notionPages[1])
|
||||
})
|
||||
|
||||
it('should change preview website page for WEB type', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.WEB,
|
||||
files: [],
|
||||
notionPages: [],
|
||||
websitePages,
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.handlePreviewChange({ id: 'https://b.com', name: 'Site B' })
|
||||
})
|
||||
expect(result.current.previewWebsitePage).toBe(websitePages[1])
|
||||
})
|
||||
|
||||
it('should not change if selected page not found (NOTION)', () => {
|
||||
const { result } = renderHook(() => usePreviewState({
|
||||
dataSourceType: DataSourceType.NOTION,
|
||||
files: [],
|
||||
notionPages,
|
||||
websitePages: [],
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.handlePreviewChange({ id: 'non-existent', name: 'x' })
|
||||
})
|
||||
expect(result.current.previewNotionPage).toBe(notionPages[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,372 @@
|
||||
import type { PreProcessingRule, Rules } from '@/models/datasets'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode, ProcessMode } from '@/models/datasets'
|
||||
import {
|
||||
DEFAULT_MAXIMUM_CHUNK_LENGTH,
|
||||
DEFAULT_OVERLAP,
|
||||
DEFAULT_SEGMENT_IDENTIFIER,
|
||||
defaultParentChildConfig,
|
||||
useSegmentationState,
|
||||
} from '../use-segmentation-state'
|
||||
|
||||
describe('useSegmentationState', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --- Default state ---
|
||||
describe('default state', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
expect(result.current.segmentationType).toBe(ProcessMode.general)
|
||||
expect(result.current.segmentIdentifier).toBe(DEFAULT_SEGMENT_IDENTIFIER)
|
||||
expect(result.current.maxChunkLength).toBe(DEFAULT_MAXIMUM_CHUNK_LENGTH)
|
||||
expect(result.current.overlap).toBe(DEFAULT_OVERLAP)
|
||||
expect(result.current.rules).toEqual([])
|
||||
expect(result.current.parentChildConfig).toEqual(defaultParentChildConfig)
|
||||
})
|
||||
|
||||
it('should accept initial segmentation type', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSegmentationState({ initialSegmentationType: ProcessMode.parentChild }),
|
||||
)
|
||||
expect(result.current.segmentationType).toBe(ProcessMode.parentChild)
|
||||
})
|
||||
|
||||
it('should accept initial summary index setting', () => {
|
||||
const setting = { enable: true }
|
||||
const { result } = renderHook(() =>
|
||||
useSegmentationState({ initialSummaryIndexSetting: setting }),
|
||||
)
|
||||
expect(result.current.summaryIndexSetting).toEqual(setting)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Setters ---
|
||||
describe('setters', () => {
|
||||
it('should update segmentation type', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setSegmentationType(ProcessMode.parentChild)
|
||||
})
|
||||
expect(result.current.segmentationType).toBe(ProcessMode.parentChild)
|
||||
})
|
||||
|
||||
it('should update max chunk length', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setMaxChunkLength(2048)
|
||||
})
|
||||
expect(result.current.maxChunkLength).toBe(2048)
|
||||
})
|
||||
|
||||
it('should update overlap', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setOverlap(100)
|
||||
})
|
||||
expect(result.current.overlap).toBe(100)
|
||||
})
|
||||
|
||||
it('should update rules', () => {
|
||||
const newRules: PreProcessingRule[] = [
|
||||
{ id: 'remove_extra_spaces', enabled: true },
|
||||
{ id: 'remove_urls_emails', enabled: false },
|
||||
]
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setRules(newRules)
|
||||
})
|
||||
expect(result.current.rules).toEqual(newRules)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Segment identifier with escaping ---
|
||||
describe('setSegmentIdentifier', () => {
|
||||
it('should escape the value when setting', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setSegmentIdentifier('\n\n')
|
||||
})
|
||||
expect(result.current.segmentIdentifier).toBe('\\n\\n')
|
||||
})
|
||||
|
||||
it('should reset to default when empty and canEmpty is false', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setSegmentIdentifier('')
|
||||
})
|
||||
expect(result.current.segmentIdentifier).toBe(DEFAULT_SEGMENT_IDENTIFIER)
|
||||
})
|
||||
|
||||
it('should allow empty value when canEmpty is true', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setSegmentIdentifier('', true)
|
||||
})
|
||||
expect(result.current.segmentIdentifier).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
// --- Toggle rule ---
|
||||
describe('toggleRule', () => {
|
||||
it('should toggle a rule enabled state', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
const rules: PreProcessingRule[] = [
|
||||
{ id: 'remove_extra_spaces', enabled: true },
|
||||
{ id: 'remove_urls_emails', enabled: false },
|
||||
]
|
||||
|
||||
act(() => {
|
||||
result.current.setRules(rules)
|
||||
})
|
||||
act(() => {
|
||||
result.current.toggleRule('remove_extra_spaces')
|
||||
})
|
||||
|
||||
expect(result.current.rules[0].enabled).toBe(false)
|
||||
expect(result.current.rules[1].enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('should toggle second rule without affecting first', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
const rules: PreProcessingRule[] = [
|
||||
{ id: 'remove_extra_spaces', enabled: true },
|
||||
{ id: 'remove_urls_emails', enabled: false },
|
||||
]
|
||||
|
||||
act(() => {
|
||||
result.current.setRules(rules)
|
||||
})
|
||||
act(() => {
|
||||
result.current.toggleRule('remove_urls_emails')
|
||||
})
|
||||
|
||||
expect(result.current.rules[0].enabled).toBe(true)
|
||||
expect(result.current.rules[1].enabled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Parent-child config ---
|
||||
describe('parent-child config', () => {
|
||||
it('should update parent delimiter with escaping', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.updateParentConfig('delimiter', '\n')
|
||||
})
|
||||
expect(result.current.parentChildConfig.parent.delimiter).toBe('\\n')
|
||||
})
|
||||
|
||||
it('should update parent maxLength', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.updateParentConfig('maxLength', 2048)
|
||||
})
|
||||
expect(result.current.parentChildConfig.parent.maxLength).toBe(2048)
|
||||
})
|
||||
|
||||
it('should update child delimiter with escaping', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.updateChildConfig('delimiter', '\t')
|
||||
})
|
||||
expect(result.current.parentChildConfig.child.delimiter).toBe('\\t')
|
||||
})
|
||||
|
||||
it('should update child maxLength', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.updateChildConfig('maxLength', 256)
|
||||
})
|
||||
expect(result.current.parentChildConfig.child.maxLength).toBe(256)
|
||||
})
|
||||
|
||||
it('should set empty delimiter when value is empty', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.updateParentConfig('delimiter', '')
|
||||
})
|
||||
expect(result.current.parentChildConfig.parent.delimiter).toBe('')
|
||||
})
|
||||
|
||||
it('should set chunk for context mode', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.setChunkForContext('full-doc')
|
||||
})
|
||||
expect(result.current.parentChildConfig.chunkForContext).toBe('full-doc')
|
||||
})
|
||||
})
|
||||
|
||||
// --- Reset to defaults ---
|
||||
describe('resetToDefaults', () => {
|
||||
it('should reset to default config when defaults are set', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
const defaultRules: Rules = {
|
||||
pre_processing_rules: [{ id: 'remove_extra_spaces', enabled: true }],
|
||||
segmentation: {
|
||||
separator: '---',
|
||||
max_tokens: 500,
|
||||
chunk_overlap: 25,
|
||||
},
|
||||
parent_mode: 'paragraph',
|
||||
subchunk_segmentation: {
|
||||
separator: '\n',
|
||||
max_tokens: 200,
|
||||
},
|
||||
}
|
||||
|
||||
act(() => {
|
||||
result.current.setDefaultConfig(defaultRules)
|
||||
})
|
||||
// Change values
|
||||
act(() => {
|
||||
result.current.setMaxChunkLength(2048)
|
||||
result.current.setOverlap(200)
|
||||
})
|
||||
act(() => {
|
||||
result.current.resetToDefaults()
|
||||
})
|
||||
|
||||
expect(result.current.maxChunkLength).toBe(500)
|
||||
expect(result.current.overlap).toBe(25)
|
||||
expect(result.current.parentChildConfig).toEqual(defaultParentChildConfig)
|
||||
})
|
||||
|
||||
it('should reset parent-child config even without default config', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
act(() => {
|
||||
result.current.updateParentConfig('maxLength', 9999)
|
||||
})
|
||||
act(() => {
|
||||
result.current.resetToDefaults()
|
||||
})
|
||||
|
||||
expect(result.current.parentChildConfig).toEqual(defaultParentChildConfig)
|
||||
})
|
||||
})
|
||||
|
||||
// --- applyConfigFromRules ---
|
||||
describe('applyConfigFromRules', () => {
|
||||
it('should apply general config from rules', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
const rulesConfig: Rules = {
|
||||
pre_processing_rules: [{ id: 'remove_extra_spaces', enabled: true }],
|
||||
segmentation: {
|
||||
separator: '|||',
|
||||
max_tokens: 800,
|
||||
chunk_overlap: 30,
|
||||
},
|
||||
parent_mode: 'paragraph',
|
||||
subchunk_segmentation: {
|
||||
separator: '\n',
|
||||
max_tokens: 200,
|
||||
},
|
||||
}
|
||||
|
||||
act(() => {
|
||||
result.current.applyConfigFromRules(rulesConfig, false)
|
||||
})
|
||||
|
||||
expect(result.current.maxChunkLength).toBe(800)
|
||||
expect(result.current.overlap).toBe(30)
|
||||
expect(result.current.rules).toEqual(rulesConfig.pre_processing_rules)
|
||||
})
|
||||
|
||||
it('should apply hierarchical config from rules', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
const rulesConfig: Rules = {
|
||||
pre_processing_rules: [],
|
||||
segmentation: {
|
||||
separator: '\n\n',
|
||||
max_tokens: 1024,
|
||||
chunk_overlap: 50,
|
||||
},
|
||||
parent_mode: 'full-doc',
|
||||
subchunk_segmentation: {
|
||||
separator: '\n',
|
||||
max_tokens: 256,
|
||||
},
|
||||
}
|
||||
|
||||
act(() => {
|
||||
result.current.applyConfigFromRules(rulesConfig, true)
|
||||
})
|
||||
|
||||
expect(result.current.parentChildConfig.chunkForContext).toBe('full-doc')
|
||||
expect(result.current.parentChildConfig.child.maxLength).toBe(256)
|
||||
})
|
||||
})
|
||||
|
||||
// --- getProcessRule ---
|
||||
describe('getProcessRule', () => {
|
||||
it('should build general process rule', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
const rule = result.current.getProcessRule(ChunkingMode.text)
|
||||
expect(rule.mode).toBe(ProcessMode.general)
|
||||
expect(rule.rules!.segmentation.max_tokens).toBe(DEFAULT_MAXIMUM_CHUNK_LENGTH)
|
||||
expect(rule.rules!.segmentation.chunk_overlap).toBe(DEFAULT_OVERLAP)
|
||||
})
|
||||
|
||||
it('should build parent-child process rule', () => {
|
||||
const { result } = renderHook(() => useSegmentationState())
|
||||
|
||||
const rule = result.current.getProcessRule(ChunkingMode.parentChild)
|
||||
expect(rule.mode).toBe('hierarchical')
|
||||
expect(rule.rules!.parent_mode).toBe('paragraph')
|
||||
expect(rule.rules!.subchunk_segmentation).toBeDefined()
|
||||
})
|
||||
|
||||
it('should include summary index setting in process rule', () => {
|
||||
const setting = { enable: true }
|
||||
const { result } = renderHook(() =>
|
||||
useSegmentationState({ initialSummaryIndexSetting: setting }),
|
||||
)
|
||||
|
||||
const rule = result.current.getProcessRule(ChunkingMode.text)
|
||||
expect(rule.summary_index_setting).toEqual(setting)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Summary index setting ---
|
||||
describe('handleSummaryIndexSettingChange', () => {
|
||||
it('should update summary index setting', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSegmentationState({ initialSummaryIndexSetting: { enable: false } }),
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleSummaryIndexSettingChange({ enable: true })
|
||||
})
|
||||
expect(result.current.summaryIndexSetting).toEqual({ enable: true })
|
||||
})
|
||||
|
||||
it('should merge with existing setting', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSegmentationState({ initialSummaryIndexSetting: { enable: true } }),
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleSummaryIndexSettingChange({ enable: false })
|
||||
})
|
||||
expect(result.current.summaryIndexSetting?.enable).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,8 +1,8 @@
|
||||
import type { ILanguageSelectProps } from './index'
|
||||
import type { ILanguageSelectProps } from '../index'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import LanguageSelect from './index'
|
||||
import LanguageSelect from '../index'
|
||||
|
||||
// Get supported languages for test assertions
|
||||
const supportedLanguages = languages.filter(lang => lang.supported)
|
||||
@ -20,37 +20,27 @@ describe('LanguageSelect', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests - Verify component renders correctly
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('English')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render current language text', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentLanguage: 'Chinese Simplified' })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Chinese Simplified')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render dropdown arrow icon', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert - RiArrowDownSLine renders as SVG
|
||||
@ -59,7 +49,6 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should render all supported languages in dropdown when opened', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
@ -75,12 +64,10 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should render check icon for selected language', () => {
|
||||
// Arrange
|
||||
const selectedLanguage = 'Japanese'
|
||||
const props = createDefaultProps({ currentLanguage: selectedLanguage })
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -91,9 +78,7 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing - Verify all prop variations work correctly
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('currentLanguage prop', () => {
|
||||
it('should display English when currentLanguage is English', () => {
|
||||
@ -126,47 +111,36 @@ describe('LanguageSelect', () => {
|
||||
|
||||
describe('disabled prop', () => {
|
||||
it('should have disabled button when disabled is true', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: true })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should have enabled button when disabled is false', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: false })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should have enabled button when disabled is undefined', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
delete (props as Partial<ILanguageSelectProps>).disabled
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should apply disabled styling when disabled is true', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: true })
|
||||
|
||||
// Act
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert - Check for disabled class on text elements
|
||||
@ -175,13 +149,10 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should apply cursor-not-allowed styling when disabled', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: true })
|
||||
|
||||
// Act
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
const elementWithCursor = container.querySelector('.cursor-not-allowed')
|
||||
expect(elementWithCursor).toBeInTheDocument()
|
||||
})
|
||||
@ -205,16 +176,12 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions - Test event handlers
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should open dropdown when button is clicked', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -223,24 +190,20 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should call onSelect when a language option is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnSelect = vi.fn()
|
||||
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
const frenchOption = screen.getByText('French')
|
||||
fireEvent.click(frenchOption)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelect).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('French')
|
||||
})
|
||||
|
||||
it('should call onSelect with correct language when selecting different languages', () => {
|
||||
// Arrange
|
||||
const mockOnSelect = vi.fn()
|
||||
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||
render(<LanguageSelect {...props} />)
|
||||
@ -259,11 +222,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should not open dropdown when disabled', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: true })
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -273,21 +234,17 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should not call onSelect when component is disabled', () => {
|
||||
// Arrange
|
||||
const mockOnSelect = vi.fn()
|
||||
const props = createDefaultProps({ onSelect: mockOnSelect, disabled: true })
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle rapid consecutive clicks', () => {
|
||||
// Arrange
|
||||
const mockOnSelect = vi.fn()
|
||||
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||
render(<LanguageSelect {...props} />)
|
||||
@ -303,9 +260,7 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Component Memoization - Test React.memo behavior
|
||||
// ==========================================
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert - Check component has memo wrapper
|
||||
@ -313,7 +268,6 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should not re-render when props remain the same', () => {
|
||||
// Arrange
|
||||
const mockOnSelect = vi.fn()
|
||||
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||
const renderSpy = vi.fn()
|
||||
@ -325,7 +279,6 @@ describe('LanguageSelect', () => {
|
||||
}
|
||||
const MemoizedTracked = React.memo(TrackedLanguageSelect)
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<MemoizedTracked {...props} />)
|
||||
rerender(<MemoizedTracked {...props} />)
|
||||
|
||||
@ -334,43 +287,33 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should re-render when currentLanguage changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentLanguage: 'English' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<LanguageSelect {...props} />)
|
||||
expect(screen.getByText('English')).toBeInTheDocument()
|
||||
|
||||
rerender(<LanguageSelect {...props} currentLanguage="French" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('French')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when disabled changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: false })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<LanguageSelect {...props} />)
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
rerender(<LanguageSelect {...props} disabled={true} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases - Test boundary conditions and error handling
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty string as currentLanguage', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentLanguage: '' })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert - Component should still render
|
||||
@ -379,10 +322,8 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should handle non-existent language as currentLanguage', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentLanguage: 'NonExistentLanguage' })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert - Should display the value even if not in list
|
||||
@ -393,19 +334,15 @@ describe('LanguageSelect', () => {
|
||||
// Arrange - Turkish has special character in prompt_name
|
||||
const props = createDefaultProps({ currentLanguage: 'Türkçe' })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Türkçe')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long language names', () => {
|
||||
// Arrange
|
||||
const longLanguageName = 'A'.repeat(100)
|
||||
const props = createDefaultProps({ currentLanguage: longLanguageName })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert - Should not crash and should display the text
|
||||
@ -413,11 +350,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should render correct number of language options', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -431,11 +366,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should only show supported languages in dropdown', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -452,7 +385,6 @@ describe('LanguageSelect', () => {
|
||||
// Arrange - This tests TypeScript boundary, but runtime should not crash
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
@ -463,11 +395,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should maintain selection state visually with check icon', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentLanguage: 'Russian' })
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -478,28 +408,21 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Accessibility - Basic accessibility checks
|
||||
// ==========================================
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible button element', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have clickable language options', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -509,16 +432,12 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Integration with Popover - Test Popover behavior
|
||||
// ==========================================
|
||||
describe('Popover Integration', () => {
|
||||
it('should use manualClose prop on Popover', () => {
|
||||
// Arrange
|
||||
const mockOnSelect = vi.fn()
|
||||
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||
|
||||
// Act
|
||||
render(<LanguageSelect {...props} />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
@ -528,11 +447,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should have correct popup z-index class', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -542,12 +459,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Styling Tests - Verify correct CSS classes applied
|
||||
// ==========================================
|
||||
describe('Styling', () => {
|
||||
it('should apply tertiary button styling', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
@ -556,11 +470,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should apply hover styling class to options', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -570,11 +482,9 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should apply correct text styling to language options', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
@ -584,7 +494,6 @@ describe('LanguageSelect', () => {
|
||||
})
|
||||
|
||||
it('should apply disabled styling to icon when disabled', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ disabled: true })
|
||||
const { container } = render(<LanguageSelect {...props} />)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { IPreviewItemProps } from './index'
|
||||
import type { IPreviewItemProps } from '../index'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import PreviewItem, { PreviewType } from './index'
|
||||
import PreviewItem, { PreviewType } from '../index'
|
||||
|
||||
// Test data builder for props
|
||||
const createDefaultProps = (overrides?: Partial<IPreviewItemProps>): IPreviewItemProps => ({
|
||||
@ -26,40 +26,29 @@ describe('PreviewItem', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests - Verify component renders correctly
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with TEXT type', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: 'Sample text content' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Sample text content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with QA type', () => {
|
||||
// Arrange
|
||||
const props = createQAProps()
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test question')).toBeInTheDocument()
|
||||
@ -67,10 +56,8 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should render sharp icon (#) with formatted index', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index: 5 })
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Index should be padded to 3 digits
|
||||
@ -81,11 +68,9 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should render character count for TEXT type', () => {
|
||||
// Arrange
|
||||
const content = 'Hello World' // 11 characters
|
||||
const props = createDefaultProps({ content })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Shows character count with translation key
|
||||
@ -94,7 +79,6 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should render character count for QA type', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
qa: {
|
||||
question: 'Hello', // 5 characters
|
||||
@ -102,7 +86,6 @@ describe('PreviewItem', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Shows combined character count
|
||||
@ -110,10 +93,8 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should render text icon SVG', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should have SVG icons
|
||||
@ -122,35 +103,27 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing - Verify all prop variations work correctly
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('type prop', () => {
|
||||
it('should render TEXT content when type is TEXT', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ type: PreviewType.TEXT, content: 'Text mode content' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Text mode content')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Q')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('A')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render QA content when type is QA', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
type: PreviewType.QA,
|
||||
qa: { question: 'My question', answer: 'My answer' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
expect(screen.getByText('My question')).toBeInTheDocument()
|
||||
@ -158,24 +131,18 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should use TEXT as default type when type is "text"', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ type: 'text' as PreviewType, content: 'Default type content' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Default type content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use QA type when type is "QA"', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({ type: 'QA' as PreviewType })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
@ -191,57 +158,43 @@ describe('PreviewItem', () => {
|
||||
[999, '999'],
|
||||
[1000, '1000'],
|
||||
])('should format index %i as %s', (index, expected) => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(expected)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle index 0', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index: 0 })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('000')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large index numbers', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index: 12345 })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('12345')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('content prop', () => {
|
||||
it('should render content when provided', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: 'Custom content here' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Custom content here')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiline content', () => {
|
||||
// Arrange
|
||||
const multilineContent = 'Line 1\nLine 2\nLine 3'
|
||||
const props = createDefaultProps({ content: multilineContent })
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Check content is rendered (multiline text is in pre-line div)
|
||||
@ -252,10 +205,8 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should preserve whitespace with pre-line style', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: 'Text with spaces' })
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Check for whiteSpace: pre-line style
|
||||
@ -266,7 +217,6 @@ describe('PreviewItem', () => {
|
||||
|
||||
describe('qa prop', () => {
|
||||
it('should render question and answer when qa is provided', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
qa: {
|
||||
question: 'What is testing?',
|
||||
@ -274,28 +224,22 @@ describe('PreviewItem', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('What is testing?')).toBeInTheDocument()
|
||||
expect(screen.getByText('Testing is verification.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Q and A labels', () => {
|
||||
// Arrange
|
||||
const props = createQAProps()
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiline question', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
qa: {
|
||||
question: 'Question line 1\nQuestion line 2',
|
||||
@ -303,7 +247,6 @@ describe('PreviewItem', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Check content is in pre-line div
|
||||
@ -314,7 +257,6 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should handle multiline answer', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
qa: {
|
||||
question: 'Question',
|
||||
@ -322,7 +264,6 @@ describe('PreviewItem', () => {
|
||||
},
|
||||
})
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Check content is in pre-line div
|
||||
@ -334,9 +275,7 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Component Memoization - Test React.memo behavior
|
||||
// ==========================================
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert - Check component has memo wrapper
|
||||
@ -344,7 +283,6 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should not re-render when props remain the same', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
const renderSpy = vi.fn()
|
||||
|
||||
@ -355,7 +293,6 @@ describe('PreviewItem', () => {
|
||||
}
|
||||
const MemoizedTracked = React.memo(TrackedPreviewItem)
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<MemoizedTracked {...props} />)
|
||||
rerender(<MemoizedTracked {...props} />)
|
||||
|
||||
@ -364,77 +301,61 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should re-render when content changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: 'Initial content' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<PreviewItem {...props} />)
|
||||
expect(screen.getByText('Initial content')).toBeInTheDocument()
|
||||
|
||||
rerender(<PreviewItem {...props} content="Updated content" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Updated content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when index changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index: 1 })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<PreviewItem {...props} />)
|
||||
expect(screen.getByText('001')).toBeInTheDocument()
|
||||
|
||||
rerender(<PreviewItem {...props} index={99} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('099')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when type changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ type: PreviewType.TEXT, content: 'Text content' })
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<PreviewItem {...props} />)
|
||||
expect(screen.getByText('Text content')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Q')).not.toBeInTheDocument()
|
||||
|
||||
rerender(<PreviewItem type={PreviewType.QA} index={1} qa={{ question: 'Q1', answer: 'A1' }} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when qa prop changes', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
qa: { question: 'Original question', answer: 'Original answer' },
|
||||
})
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<PreviewItem {...props} />)
|
||||
expect(screen.getByText('Original question')).toBeInTheDocument()
|
||||
|
||||
rerender(<PreviewItem {...props} qa={{ question: 'New question', answer: 'New answer' }} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('New question')).toBeInTheDocument()
|
||||
expect(screen.getByText('New answer')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases - Test boundary conditions and error handling
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
describe('Empty/Undefined values', () => {
|
||||
it('should handle undefined content gracefully', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: undefined })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should show 0 characters (use more specific text match)
|
||||
@ -442,10 +363,8 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should handle empty string content', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: '' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should show 0 characters (use more specific text match)
|
||||
@ -453,14 +372,12 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should handle undefined qa gracefully', () => {
|
||||
// Arrange
|
||||
const props: IPreviewItemProps = {
|
||||
type: PreviewType.QA,
|
||||
index: 1,
|
||||
qa: undefined,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should render Q and A labels but with empty content
|
||||
@ -471,7 +388,6 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should handle undefined question in qa', () => {
|
||||
// Arrange
|
||||
const props: IPreviewItemProps = {
|
||||
type: PreviewType.QA,
|
||||
index: 1,
|
||||
@ -481,15 +397,12 @@ describe('PreviewItem', () => {
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Only answer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined answer in qa', () => {
|
||||
// Arrange
|
||||
const props: IPreviewItemProps = {
|
||||
type: PreviewType.QA,
|
||||
index: 1,
|
||||
@ -499,20 +412,16 @@ describe('PreviewItem', () => {
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Only question')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty question and answer strings', () => {
|
||||
// Arrange
|
||||
const props = createQAProps({
|
||||
qa: { question: '', answer: '' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should show 0 characters (use more specific text match)
|
||||
@ -527,10 +436,8 @@ describe('PreviewItem', () => {
|
||||
// Arrange - 'Test' has 4 characters
|
||||
const props = createDefaultProps({ content: 'Test' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/4/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -540,10 +447,8 @@ describe('PreviewItem', () => {
|
||||
qa: { question: 'ABC', answer: 'DEFGH' },
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/8/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -551,10 +456,8 @@ describe('PreviewItem', () => {
|
||||
// Arrange - Content with special characters
|
||||
const props = createDefaultProps({ content: '你好世界' }) // 4 Chinese characters
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/4/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -562,10 +465,8 @@ describe('PreviewItem', () => {
|
||||
// Arrange - 'a\nb' has 3 characters
|
||||
const props = createDefaultProps({ content: 'a\nb' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/3/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -573,21 +474,17 @@ describe('PreviewItem', () => {
|
||||
// Arrange - 'a b' has 3 characters
|
||||
const props = createDefaultProps({ content: 'a b' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/3/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Boundary conditions', () => {
|
||||
it('should handle very long content', () => {
|
||||
// Arrange
|
||||
const longContent = 'A'.repeat(10000)
|
||||
const props = createDefaultProps({ content: longContent })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should show correct character count
|
||||
@ -595,21 +492,16 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should handle very long index', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index: 999999999 })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('999999999')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle negative index', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ index: -1 })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - padStart pads from the start, so -1 becomes 0-1
|
||||
@ -617,21 +509,16 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should handle content with only whitespace', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: ' ' }) // 3 spaces
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/3/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle content with HTML-like characters', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: '<div>Test</div>' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should render as text, not HTML
|
||||
@ -642,7 +529,6 @@ describe('PreviewItem', () => {
|
||||
// Arrange - Emojis can have complex character lengths
|
||||
const props = createDefaultProps({ content: '😀👍' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Emoji length depends on JS string length
|
||||
@ -660,17 +546,14 @@ describe('PreviewItem', () => {
|
||||
qa: { question: 'Should not show', answer: 'Also should not show' },
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Text content')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Should not show')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Also should not show')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use content length for TEXT type even when qa is provided', () => {
|
||||
// Arrange
|
||||
const props: IPreviewItemProps = {
|
||||
type: PreviewType.TEXT,
|
||||
index: 1,
|
||||
@ -678,7 +561,6 @@ describe('PreviewItem', () => {
|
||||
qa: { question: 'Question', answer: 'Answer' }, // Would be 14 characters if used
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Should show 2, not 14
|
||||
@ -686,7 +568,6 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should ignore content prop when type is QA', () => {
|
||||
// Arrange
|
||||
const props: IPreviewItemProps = {
|
||||
type: PreviewType.QA,
|
||||
index: 1,
|
||||
@ -694,10 +575,8 @@ describe('PreviewItem', () => {
|
||||
qa: { question: 'Q text', answer: 'A text' },
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('Should not display')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Q text')).toBeInTheDocument()
|
||||
expect(screen.getByText('A text')).toBeInTheDocument()
|
||||
@ -705,9 +584,7 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// PreviewType Enum - Test exported enum values
|
||||
// ==========================================
|
||||
describe('PreviewType Enum', () => {
|
||||
it('should have TEXT value as "text"', () => {
|
||||
expect(PreviewType.TEXT).toBe('text')
|
||||
@ -718,27 +595,20 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Styling Tests - Verify correct CSS classes applied
|
||||
// ==========================================
|
||||
describe('Styling', () => {
|
||||
it('should have rounded container with gray background', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
const rootDiv = container.firstChild as HTMLElement
|
||||
expect(rootDiv).toHaveClass('rounded-xl', 'bg-gray-50', 'p-4')
|
||||
})
|
||||
|
||||
it('should have proper header styling', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - Check header div styling
|
||||
@ -747,53 +617,40 @@ describe('PreviewItem', () => {
|
||||
})
|
||||
|
||||
it('should have index badge styling', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
const indexBadge = container.querySelector('.border.border-gray-200')
|
||||
expect(indexBadge).toBeInTheDocument()
|
||||
expect(indexBadge).toHaveClass('rounded-md', 'italic', 'font-medium')
|
||||
})
|
||||
|
||||
it('should have content area with line-clamp', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
const contentArea = container.querySelector('.line-clamp-6')
|
||||
expect(contentArea).toBeInTheDocument()
|
||||
expect(contentArea).toHaveClass('max-h-[120px]', 'overflow-hidden')
|
||||
})
|
||||
|
||||
it('should have Q/A labels with gray color', () => {
|
||||
// Arrange
|
||||
const props = createQAProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert
|
||||
const labels = container.querySelectorAll('.text-gray-400')
|
||||
expect(labels.length).toBeGreaterThanOrEqual(2) // Q and A labels
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// i18n Translation - Test translation integration
|
||||
// ==========================================
|
||||
describe('i18n Translation', () => {
|
||||
it('should use translation key for characters label', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ content: 'Test' })
|
||||
|
||||
// Act
|
||||
render(<PreviewItem {...props} />)
|
||||
|
||||
// Assert - The mock returns the key as-is
|
||||
@ -1,8 +1,8 @@
|
||||
import type { StepperProps } from './index'
|
||||
import type { Step, StepperStepProps } from './step'
|
||||
import type { StepperProps } from '../index'
|
||||
import type { Step, StepperStepProps } from '../step'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { Stepper } from './index'
|
||||
import { StepperStep } from './step'
|
||||
import { Stepper } from '../index'
|
||||
import { StepperStep } from '../step'
|
||||
|
||||
// Test data factory for creating steps
|
||||
const createStep = (overrides: Partial<Step> = {}): Step => ({
|
||||
@ -34,44 +34,33 @@ const renderStepperStep = (props: Partial<StepperStepProps> = {}) => {
|
||||
return render(<StepperStep {...defaultProps} />)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stepper Component Tests
|
||||
// ============================================================================
|
||||
describe('Stepper', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests - Verify component renders properly with various inputs
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderStepper()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Step 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all step names', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(3, 'Custom Step')
|
||||
|
||||
// Act
|
||||
renderStepper({ steps })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Custom Step 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Custom Step 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Custom Step 3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render dividers between steps', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(3)
|
||||
|
||||
// Act
|
||||
const { container } = renderStepper({ steps })
|
||||
|
||||
// Assert - Should have 2 dividers for 3 steps
|
||||
@ -80,10 +69,8 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should not render divider after last step', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(2)
|
||||
|
||||
// Act
|
||||
const { container } = renderStepper({ steps })
|
||||
|
||||
// Assert - Should have 1 divider for 2 steps
|
||||
@ -92,28 +79,21 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should render with flex container layout', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepper()
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('flex', 'items-center', 'gap-3')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing - Test all prop variations and combinations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('steps prop', () => {
|
||||
it('should render correct number of steps', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(5)
|
||||
|
||||
// Act
|
||||
renderStepper({ steps })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Step 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Step 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Step 3')).toBeInTheDocument()
|
||||
@ -122,13 +102,10 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should handle single step correctly', () => {
|
||||
// Arrange
|
||||
const steps = [createStep({ name: 'Only Step' })]
|
||||
|
||||
// Act
|
||||
const { container } = renderStepper({ steps, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Only Step')).toBeInTheDocument()
|
||||
// No dividers for single step
|
||||
const dividers = container.querySelectorAll('.bg-divider-deep')
|
||||
@ -136,29 +113,23 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should handle steps with long names', () => {
|
||||
// Arrange
|
||||
const longName = 'This is a very long step name that might overflow'
|
||||
const steps = [createStep({ name: longName })]
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longName)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle steps with special characters', () => {
|
||||
// Arrange
|
||||
const steps = [
|
||||
createStep({ name: 'Step & Configuration' }),
|
||||
createStep({ name: 'Step <Preview>' }),
|
||||
createStep({ name: 'Step "Complete"' }),
|
||||
]
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Step & Configuration')).toBeInTheDocument()
|
||||
expect(screen.getByText('Step <Preview>')).toBeInTheDocument()
|
||||
expect(screen.getByText('Step "Complete"')).toBeInTheDocument()
|
||||
@ -167,7 +138,6 @@ describe('Stepper', () => {
|
||||
|
||||
describe('activeIndex prop', () => {
|
||||
it('should highlight first step when activeIndex is 0', () => {
|
||||
// Arrange & Act
|
||||
renderStepper({ activeIndex: 0 })
|
||||
|
||||
// Assert - First step should show "STEP 1" label
|
||||
@ -175,7 +145,6 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should highlight second step when activeIndex is 1', () => {
|
||||
// Arrange & Act
|
||||
renderStepper({ activeIndex: 1 })
|
||||
|
||||
// Assert - Second step should show "STEP 2" label
|
||||
@ -183,10 +152,8 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should highlight last step when activeIndex equals steps length - 1', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(3)
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 2 })
|
||||
|
||||
// Assert - Third step should show "STEP 3" label
|
||||
@ -194,10 +161,8 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should show completed steps with number only (no STEP prefix)', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(3)
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 2 })
|
||||
|
||||
// Assert - Completed steps show just the number
|
||||
@ -207,10 +172,8 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should show disabled steps with number only (no STEP prefix)', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(3)
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 0 })
|
||||
|
||||
// Assert - Disabled steps show just the number
|
||||
@ -221,12 +184,9 @@ describe('Stepper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases - Test boundary conditions and unexpected inputs
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty steps array', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepper({ steps: [] })
|
||||
|
||||
// Assert - Container should render but be empty
|
||||
@ -235,7 +195,6 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should handle activeIndex greater than steps length', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(2)
|
||||
|
||||
// Act - activeIndex 5 is beyond array bounds
|
||||
@ -247,7 +206,6 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should handle negative activeIndex', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(2)
|
||||
|
||||
// Act - negative activeIndex
|
||||
@ -259,13 +217,10 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should handle large number of steps', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(10)
|
||||
|
||||
// Act
|
||||
const { container } = renderStepper({ steps, activeIndex: 5 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 6')).toBeInTheDocument()
|
||||
// Should have 9 dividers for 10 steps
|
||||
const dividers = container.querySelectorAll('.bg-divider-deep')
|
||||
@ -273,10 +228,8 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should handle steps with empty name', () => {
|
||||
// Arrange
|
||||
const steps = [createStep({ name: '' })]
|
||||
|
||||
// Act
|
||||
const { container } = renderStepper({ steps, activeIndex: 0 })
|
||||
|
||||
// Assert - Should still render the step structure
|
||||
@ -285,18 +238,13 @@ describe('Stepper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Integration - Test step state combinations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Step States', () => {
|
||||
it('should render mixed states: completed, active, disabled', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(5)
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 2 })
|
||||
|
||||
// Assert
|
||||
// Steps 1-2 are completed (show number only)
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
@ -308,7 +256,6 @@ describe('Stepper', () => {
|
||||
})
|
||||
|
||||
it('should transition through all states correctly', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(3)
|
||||
|
||||
// Act & Assert - Step 1 active
|
||||
@ -329,80 +276,59 @@ describe('Stepper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// StepperStep Component Tests
|
||||
// ============================================================================
|
||||
describe('StepperStep', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Step')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render step name', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ name: 'Configure Dataset' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Configure Dataset')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with flex container layout', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep()
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('flex', 'items-center', 'gap-2')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Active State Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Active State', () => {
|
||||
it('should show STEP prefix when active', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 0, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply active styles to label container', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 0, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
const labelContainer = container.querySelector('.bg-state-accent-solid')
|
||||
expect(labelContainer).toBeInTheDocument()
|
||||
expect(labelContainer).toHaveClass('px-2')
|
||||
})
|
||||
|
||||
it('should apply active text color to label', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 0, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
const label = container.querySelector('.text-text-primary-on-surface')
|
||||
expect(label).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply accent text color to name when active', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 0, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
const nameElement = container.querySelector('.text-text-accent')
|
||||
expect(nameElement).toBeInTheDocument()
|
||||
expect(nameElement).toHaveClass('system-xs-semibold-uppercase')
|
||||
@ -421,105 +347,79 @@ describe('StepperStep', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Completed State Tests (index < activeIndex)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Completed State', () => {
|
||||
it('should show number only when completed (not active)', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 0, activeIndex: 1 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
expect(screen.queryByText('STEP 1')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply completed styles to label container', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 0, activeIndex: 1 })
|
||||
|
||||
// Assert
|
||||
const labelContainer = container.querySelector('.border-text-quaternary')
|
||||
expect(labelContainer).toBeInTheDocument()
|
||||
expect(labelContainer).toHaveClass('w-5')
|
||||
})
|
||||
|
||||
it('should apply tertiary text color to label when completed', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 0, activeIndex: 1 })
|
||||
|
||||
// Assert
|
||||
const label = container.querySelector('.text-text-tertiary')
|
||||
expect(label).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply tertiary text color to name when completed', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 0, activeIndex: 2 })
|
||||
|
||||
// Assert
|
||||
const nameElements = container.querySelectorAll('.text-text-tertiary')
|
||||
expect(nameElements.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Disabled State Tests (index > activeIndex)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Disabled State', () => {
|
||||
it('should show number only when disabled', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 2, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('3')).toBeInTheDocument()
|
||||
expect(screen.queryByText('STEP 3')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply disabled styles to label container', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 2, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
const labelContainer = container.querySelector('.border-divider-deep')
|
||||
expect(labelContainer).toBeInTheDocument()
|
||||
expect(labelContainer).toHaveClass('w-5')
|
||||
})
|
||||
|
||||
it('should apply quaternary text color to label when disabled', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 2, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
const label = container.querySelector('.text-text-quaternary')
|
||||
expect(label).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply quaternary text color to name when disabled', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ index: 2, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
const nameElements = container.querySelectorAll('.text-text-quaternary')
|
||||
expect(nameElements.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('name prop', () => {
|
||||
it('should render provided name', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ name: 'Custom Name' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Custom Name')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty name', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep({ name: '' })
|
||||
|
||||
// Assert - Label should still render
|
||||
@ -528,36 +428,28 @@ describe('StepperStep', () => {
|
||||
})
|
||||
|
||||
it('should handle name with whitespace', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ name: ' Padded Name ' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Padded Name')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('index prop', () => {
|
||||
it('should display correct 1-based number for index 0', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 0, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display correct 1-based number for index 9', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 9, activeIndex: 9 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 10')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large index values', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 99, activeIndex: 99 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 100')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -581,20 +473,14 @@ describe('StepperStep', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle zero index correctly', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 0, activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle negative activeIndex', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 0, activeIndex: -1 })
|
||||
|
||||
// Assert - Step should be disabled (index > activeIndex)
|
||||
@ -602,7 +488,6 @@ describe('StepperStep', () => {
|
||||
})
|
||||
|
||||
it('should handle equal boundary (index equals activeIndex)', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ index: 5, activeIndex: 5 })
|
||||
|
||||
// Assert - Should be active
|
||||
@ -610,7 +495,6 @@ describe('StepperStep', () => {
|
||||
})
|
||||
|
||||
it('should handle name with HTML-like content safely', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ name: '<script>alert("xss")</script>' })
|
||||
|
||||
// Assert - Should render as text, not execute
|
||||
@ -618,73 +502,57 @@ describe('StepperStep', () => {
|
||||
})
|
||||
|
||||
it('should handle name with unicode characters', () => {
|
||||
// Arrange & Act
|
||||
renderStepperStep({ name: 'Step 数据 🚀' })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Step 数据 🚀')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Style Classes Verification
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Style Classes', () => {
|
||||
it('should apply correct typography classes to label', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep()
|
||||
|
||||
// Assert
|
||||
const label = container.querySelector('.system-2xs-semibold-uppercase')
|
||||
expect(label).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply correct typography classes to name', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep()
|
||||
|
||||
// Assert
|
||||
const name = container.querySelector('.system-xs-medium-uppercase')
|
||||
expect(name).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have rounded pill shape for label container', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep()
|
||||
|
||||
// Assert
|
||||
const labelContainer = container.querySelector('.rounded-3xl')
|
||||
expect(labelContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply h-5 height to label container', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderStepperStep()
|
||||
|
||||
// Assert
|
||||
const labelContainer = container.querySelector('.h-5')
|
||||
expect(labelContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests - Stepper and StepperStep working together
|
||||
// ============================================================================
|
||||
describe('Stepper Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should pass correct props to each StepperStep', () => {
|
||||
// Arrange
|
||||
const steps = [
|
||||
createStep({ name: 'First' }),
|
||||
createStep({ name: 'Second' }),
|
||||
createStep({ name: 'Third' }),
|
||||
]
|
||||
|
||||
// Act
|
||||
renderStepper({ steps, activeIndex: 1 })
|
||||
|
||||
// Assert - Each step receives correct index and displays correctly
|
||||
@ -697,10 +565,8 @@ describe('Stepper Integration', () => {
|
||||
})
|
||||
|
||||
it('should maintain correct visual hierarchy across steps', () => {
|
||||
// Arrange
|
||||
const steps = createSteps(4)
|
||||
|
||||
// Act
|
||||
const { container } = renderStepper({ steps, activeIndex: 2 })
|
||||
|
||||
// Assert - Check visual hierarchy
|
||||
@ -718,10 +584,8 @@ describe('Stepper Integration', () => {
|
||||
})
|
||||
|
||||
it('should render correctly with dynamic step updates', () => {
|
||||
// Arrange
|
||||
const initialSteps = createSteps(2)
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<Stepper steps={initialSteps} activeIndex={0} />)
|
||||
expect(screen.getByText('Step 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Step 2')).toBeInTheDocument()
|
||||
@ -730,7 +594,6 @@ describe('Stepper Integration', () => {
|
||||
const updatedSteps = createSteps(4)
|
||||
rerender(<Stepper steps={updatedSteps} activeIndex={2} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('STEP 3')).toBeInTheDocument()
|
||||
expect(screen.getByText('Step 4')).toBeInTheDocument()
|
||||
})
|
||||
@ -0,0 +1,32 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { StepperStep } from '../step'
|
||||
|
||||
describe('StepperStep', () => {
|
||||
it('should render step name', () => {
|
||||
render(<StepperStep name="Configure" index={0} activeIndex={0} />)
|
||||
expect(screen.getByText('Configure')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show "STEP N" label for active step', () => {
|
||||
render(<StepperStep name="Configure" index={1} activeIndex={1} />)
|
||||
expect(screen.getByText('STEP 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show just number for non-active step', () => {
|
||||
render(<StepperStep name="Configure" index={1} activeIndex={0} />)
|
||||
expect(screen.getByText('2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply accent style for active step', () => {
|
||||
render(<StepperStep name="Step A" index={0} activeIndex={0} />)
|
||||
const nameEl = screen.getByText('Step A')
|
||||
expect(nameEl.className).toContain('text-text-accent')
|
||||
})
|
||||
|
||||
it('should apply disabled style for future step', () => {
|
||||
render(<StepperStep name="Step C" index={2} activeIndex={0} />)
|
||||
const nameEl = screen.getByText('Step C')
|
||||
expect(nameEl.className).toContain('text-text-quaternary')
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import type { MockInstance } from 'vitest'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import StopEmbeddingModal from './index'
|
||||
import StopEmbeddingModal from '../index'
|
||||
|
||||
// Helper type for component props
|
||||
type StopEmbeddingModalProps = {
|
||||
@ -23,9 +23,7 @@ const renderStopEmbeddingModal = (props: Partial<StopEmbeddingModalProps> = {})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// StopEmbeddingModal Component Tests
|
||||
// ============================================================================
|
||||
describe('StopEmbeddingModal', () => {
|
||||
// Suppress Headless UI warnings in tests
|
||||
// These warnings are from the library's internal behavior, not our code
|
||||
@ -37,69 +35,54 @@ describe('StopEmbeddingModal', () => {
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn())
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
consoleWarnSpy.mockRestore()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests - Verify component renders properly
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing when show is true', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal title', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal content', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render confirm button with correct text', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelButtonConfirm')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render cancel button with correct text', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelButtonCancel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render modal content when show is false', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepThree.modelTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render buttons in correct order (cancel first, then confirm)', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert - Due to flex-row-reverse, confirm appears first visually but cancel is first in DOM
|
||||
@ -108,25 +91,20 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should render confirm button with primary variant styling', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
expect(confirmButton).toHaveClass('ml-2', 'w-24')
|
||||
})
|
||||
|
||||
it('should render cancel button with default styling', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
|
||||
expect(cancelButton).toHaveClass('w-24')
|
||||
})
|
||||
|
||||
it('should render all modal elements', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert - Modal should contain title, content, and buttons
|
||||
@ -137,39 +115,30 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing - Test all prop variations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('show prop', () => {
|
||||
it('should show modal when show is true', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide modal when show is false', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepThree.modelTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use default value false when show is not provided', () => {
|
||||
// Arrange & Act
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
render(<StopEmbeddingModal onConfirm={onConfirm} onHide={onHide} show={false} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepThree.modelTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle visibility when show prop changes to true', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
|
||||
@ -193,10 +162,8 @@ describe('StopEmbeddingModal', () => {
|
||||
|
||||
describe('onConfirm prop', () => {
|
||||
it('should accept onConfirm callback function', () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
// Act
|
||||
renderStopEmbeddingModal({ onConfirm })
|
||||
|
||||
// Assert - No errors thrown
|
||||
@ -206,10 +173,8 @@ describe('StopEmbeddingModal', () => {
|
||||
|
||||
describe('onHide prop', () => {
|
||||
it('should accept onHide callback function', () => {
|
||||
// Arrange
|
||||
const onHide = vi.fn()
|
||||
|
||||
// Act
|
||||
renderStopEmbeddingModal({ onHide })
|
||||
|
||||
// Assert - No errors thrown
|
||||
@ -218,51 +183,41 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// User Interactions Tests - Test click events and event handlers
|
||||
// --------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
describe('Confirm Button', () => {
|
||||
it('should call onConfirm when confirm button is clicked', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onHide when confirm button is clicked', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call both onConfirm and onHide in correct order when confirm button is clicked', async () => {
|
||||
// Arrange
|
||||
const callOrder: string[] = []
|
||||
const onConfirm = vi.fn(() => callOrder.push('confirm'))
|
||||
const onHide = vi.fn(() => callOrder.push('hide'))
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
@ -273,12 +228,10 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should handle multiple clicks on confirm button', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
@ -286,7 +239,6 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).toHaveBeenCalledTimes(3)
|
||||
expect(onHide).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
@ -294,51 +246,42 @@ describe('StopEmbeddingModal', () => {
|
||||
|
||||
describe('Cancel Button', () => {
|
||||
it('should call onHide when cancel button is clicked', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
|
||||
await act(async () => {
|
||||
fireEvent.click(cancelButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onConfirm when cancel button is clicked', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
|
||||
await act(async () => {
|
||||
fireEvent.click(cancelButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle multiple clicks on cancel button', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
|
||||
await act(async () => {
|
||||
fireEvent.click(cancelButton)
|
||||
fireEvent.click(cancelButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledTimes(2)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -346,7 +289,6 @@ describe('StopEmbeddingModal', () => {
|
||||
|
||||
describe('Close Icon', () => {
|
||||
it('should call onHide when close span is clicked', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
const { container } = renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
@ -362,7 +304,6 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(closeSpan)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
else {
|
||||
@ -372,12 +313,10 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should not call onConfirm when close span is clicked', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
const { container } = renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const spans = container.querySelectorAll('span')
|
||||
const closeSpan = Array.from(spans).find(span =>
|
||||
span.className && span.getAttribute('class')?.includes('close'),
|
||||
@ -388,7 +327,6 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(closeSpan)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
@ -396,7 +334,6 @@ describe('StopEmbeddingModal', () => {
|
||||
|
||||
describe('Different Close Methods', () => {
|
||||
it('should distinguish between confirm and cancel actions', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
@ -407,11 +344,9 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(cancelButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Reset
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Act - Click confirm
|
||||
@ -420,19 +355,15 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases Tests - Test null, undefined, empty values and boundaries
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle rapid confirm button clicks', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
@ -444,13 +375,11 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).toHaveBeenCalledTimes(10)
|
||||
expect(onHide).toHaveBeenCalledTimes(10)
|
||||
})
|
||||
|
||||
it('should handle rapid cancel button clicks', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
@ -462,19 +391,16 @@ describe('StopEmbeddingModal', () => {
|
||||
fireEvent.click(cancelButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledTimes(10)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle callbacks being replaced', async () => {
|
||||
// Arrange
|
||||
const onConfirm1 = vi.fn()
|
||||
const onHide1 = vi.fn()
|
||||
const onConfirm2 = vi.fn()
|
||||
const onHide2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<StopEmbeddingModal show={true} onConfirm={onConfirm1} onHide={onHide1} />,
|
||||
)
|
||||
@ -484,7 +410,6 @@ describe('StopEmbeddingModal', () => {
|
||||
rerender(<StopEmbeddingModal show={true} onConfirm={onConfirm2} onHide={onHide2} />)
|
||||
})
|
||||
|
||||
// Click confirm with new callbacks
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
@ -498,7 +423,6 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should render with all required props', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<StopEmbeddingModal
|
||||
show={true}
|
||||
@ -507,50 +431,38 @@ describe('StopEmbeddingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Layout and Styling Tests - Verify correct structure
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Layout and Styling', () => {
|
||||
it('should have buttons container with flex-row-reverse', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons[0].closest('div')).toHaveClass('flex', 'flex-row-reverse')
|
||||
})
|
||||
|
||||
it('should render title and content elements', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render two buttons', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// submit Function Tests - Test the internal submit function behavior
|
||||
// --------------------------------------------------------------------------
|
||||
describe('submit Function', () => {
|
||||
it('should execute onConfirm first then onHide', async () => {
|
||||
// Arrange
|
||||
let confirmTime = 0
|
||||
let hideTime = 0
|
||||
let counter = 0
|
||||
@ -562,73 +474,59 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(confirmTime).toBe(1)
|
||||
expect(hideTime).toBe(2)
|
||||
})
|
||||
|
||||
it('should call both callbacks exactly once per click', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should pass no arguments to onConfirm', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onConfirm).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
it('should pass no arguments to onHide when called from submit', async () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
|
||||
await act(async () => {
|
||||
fireEvent.click(confirmButton)
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(onHide).toHaveBeenCalledWith()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Modal Integration Tests - Verify Modal component integration
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Modal Integration', () => {
|
||||
it('should pass show prop to Modal as isShow', async () => {
|
||||
// Arrange & Act
|
||||
const { rerender } = render(
|
||||
<StopEmbeddingModal show={true} onConfirm={vi.fn()} onHide={vi.fn()} />,
|
||||
)
|
||||
@ -648,15 +546,10 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Accessibility Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have buttons that are focusable', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
const buttons = screen.getAllByRole('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button).not.toHaveAttribute('tabindex', '-1')
|
||||
@ -664,19 +557,15 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should have semantic button elements', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should have accessible text content', () => {
|
||||
// Arrange & Act
|
||||
renderStopEmbeddingModal({ show: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeVisible()
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeVisible()
|
||||
expect(screen.getByText('datasetCreation.stepThree.modelButtonConfirm')).toBeVisible()
|
||||
@ -684,12 +573,9 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Component Lifecycle Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Component Lifecycle', () => {
|
||||
it('should unmount cleanly', () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
const { unmount } = renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
@ -699,12 +585,10 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should not call callbacks after unmount', () => {
|
||||
// Arrange
|
||||
const onConfirm = vi.fn()
|
||||
const onHide = vi.fn()
|
||||
const { unmount } = renderStopEmbeddingModal({ onConfirm, onHide })
|
||||
|
||||
// Act
|
||||
unmount()
|
||||
|
||||
// Assert - No callbacks should be called after unmount
|
||||
@ -713,7 +597,6 @@ describe('StopEmbeddingModal', () => {
|
||||
})
|
||||
|
||||
it('should re-render correctly when props update', async () => {
|
||||
// Arrange
|
||||
const onConfirm1 = vi.fn()
|
||||
const onHide1 = vi.fn()
|
||||
const onConfirm2 = vi.fn()
|
||||
@ -1,6 +1,6 @@
|
||||
import type { TopBarProps } from './index'
|
||||
import type { TopBarProps } from '../index'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { TopBar } from './index'
|
||||
import { TopBar } from '../index'
|
||||
|
||||
// Mock next/link to capture href values
|
||||
vi.mock('next/link', () => ({
|
||||
@ -23,31 +23,23 @@ const renderTopBar = (props: Partial<TopBarProps> = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TopBar Component Tests
|
||||
// ============================================================================
|
||||
describe('TopBar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests - Verify component renders properly
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('back-link')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render back link with arrow icon', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar()
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toBeInTheDocument()
|
||||
// Check for the arrow icon (svg element)
|
||||
@ -56,15 +48,12 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should render fallback route text', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.steps.header.fallbackRoute')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Stepper component with 3 steps', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert - Check for step translations
|
||||
@ -74,10 +63,8 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should apply default container classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar()
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('relative')
|
||||
expect(wrapper).toHaveClass('flex')
|
||||
@ -90,25 +77,19 @@ describe('TopBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Testing - Test all prop variations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
describe('className prop', () => {
|
||||
it('should apply custom className when provided', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ className: 'custom-class' })
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should merge custom className with default classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ className: 'my-custom-class another-class' })
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('relative')
|
||||
expect(wrapper).toHaveClass('flex')
|
||||
@ -117,20 +98,16 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should render correctly without className', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ className: undefined })
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('relative')
|
||||
expect(wrapper).toHaveClass('flex')
|
||||
})
|
||||
|
||||
it('should handle empty string className', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ className: '' })
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('relative')
|
||||
})
|
||||
@ -138,34 +115,27 @@ describe('TopBar', () => {
|
||||
|
||||
describe('datasetId prop', () => {
|
||||
it('should set fallback route to /datasets when datasetId is undefined', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ datasetId: undefined })
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('href', '/datasets')
|
||||
})
|
||||
|
||||
it('should set fallback route to /datasets/:id/documents when datasetId is provided', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ datasetId: 'dataset-123' })
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('href', '/datasets/dataset-123/documents')
|
||||
})
|
||||
|
||||
it('should handle various datasetId formats', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ datasetId: 'abc-def-ghi-123' })
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('href', '/datasets/abc-def-ghi-123/documents')
|
||||
})
|
||||
|
||||
it('should handle empty string datasetId', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ datasetId: '' })
|
||||
|
||||
// Assert - Empty string is falsy, so fallback to /datasets
|
||||
@ -176,7 +146,6 @@ describe('TopBar', () => {
|
||||
|
||||
describe('activeIndex prop', () => {
|
||||
it('should pass activeIndex to Stepper component (index 0)', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert - First step should be active (has specific styling)
|
||||
@ -185,7 +154,6 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should pass activeIndex to Stepper component (index 1)', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 1 })
|
||||
|
||||
// Assert - Stepper is rendered with correct props
|
||||
@ -194,15 +162,12 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should pass activeIndex to Stepper component (index 2)', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 2 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.steps.three')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle edge case activeIndex of -1', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ activeIndex: -1 })
|
||||
|
||||
// Assert - Component should render without crashing
|
||||
@ -210,7 +175,6 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should handle edge case activeIndex beyond steps length', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ activeIndex: 10 })
|
||||
|
||||
// Assert - Component should render without crashing
|
||||
@ -219,15 +183,12 @@ describe('TopBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests - Test useMemo logic and dependencies
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization Logic', () => {
|
||||
it('should compute fallbackRoute based on datasetId', () => {
|
||||
// Arrange & Act - With datasetId
|
||||
const { rerender } = render(<TopBar activeIndex={0} datasetId="test-id" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/test-id/documents')
|
||||
|
||||
// Act - Rerender with different datasetId
|
||||
@ -238,35 +199,27 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should update fallbackRoute when datasetId changes from undefined to defined', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<TopBar activeIndex={0} />)
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets')
|
||||
|
||||
// Act
|
||||
rerender(<TopBar activeIndex={0} datasetId="new-dataset" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/new-dataset/documents')
|
||||
})
|
||||
|
||||
it('should update fallbackRoute when datasetId changes from defined to undefined', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<TopBar activeIndex={0} datasetId="existing-id" />)
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/existing-id/documents')
|
||||
|
||||
// Act
|
||||
rerender(<TopBar activeIndex={0} datasetId={undefined} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets')
|
||||
})
|
||||
|
||||
it('should not change fallbackRoute when activeIndex changes but datasetId stays same', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<TopBar activeIndex={0} datasetId="stable-id" />)
|
||||
const initialHref = screen.getByTestId('back-link').getAttribute('href')
|
||||
|
||||
// Act
|
||||
rerender(<TopBar activeIndex={1} datasetId="stable-id" />)
|
||||
|
||||
// Assert - href should remain the same
|
||||
@ -274,11 +227,9 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should not change fallbackRoute when className changes but datasetId stays same', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<TopBar activeIndex={0} datasetId="stable-id" className="class-1" />)
|
||||
const initialHref = screen.getByTestId('back-link').getAttribute('href')
|
||||
|
||||
// Act
|
||||
rerender(<TopBar activeIndex={0} datasetId="stable-id" className="class-2" />)
|
||||
|
||||
// Assert - href should remain the same
|
||||
@ -286,24 +237,18 @@ describe('TopBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Link Component Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Link Component', () => {
|
||||
it('should render Link with replace prop', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar()
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('data-replace', 'true')
|
||||
})
|
||||
|
||||
it('should render Link with correct classes', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar()
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveClass('inline-flex')
|
||||
expect(backLink).toHaveClass('h-12')
|
||||
@ -316,84 +261,63 @@ describe('TopBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// STEP_T_MAP Tests - Verify step translations
|
||||
// --------------------------------------------------------------------------
|
||||
describe('STEP_T_MAP Translations', () => {
|
||||
it('should render step one translation', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render step two translation', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 1 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.steps.two')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render step three translation', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 2 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.steps.three')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render all three step translations', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.steps.two')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.steps.three')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases and Error Handling Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle special characters in datasetId', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ datasetId: 'dataset-with-special_chars.123' })
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('href', '/datasets/dataset-with-special_chars.123/documents')
|
||||
})
|
||||
|
||||
it('should handle very long datasetId', () => {
|
||||
// Arrange
|
||||
const longId = 'a'.repeat(100)
|
||||
|
||||
// Act
|
||||
renderTopBar({ datasetId: longId })
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('href', `/datasets/${longId}/documents`)
|
||||
})
|
||||
|
||||
it('should handle UUID format datasetId', () => {
|
||||
// Arrange
|
||||
const uuid = '550e8400-e29b-41d4-a716-446655440000'
|
||||
|
||||
// Act
|
||||
renderTopBar({ datasetId: uuid })
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toHaveAttribute('href', `/datasets/${uuid}/documents`)
|
||||
})
|
||||
|
||||
it('should handle whitespace in className', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ className: ' spaced-class ' })
|
||||
|
||||
// Assert - classNames utility handles whitespace
|
||||
@ -402,35 +326,28 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should render correctly with all props provided', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({
|
||||
className: 'custom-class',
|
||||
datasetId: 'full-props-id',
|
||||
activeIndex: 2,
|
||||
})
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('custom-class')
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/full-props-id/documents')
|
||||
})
|
||||
|
||||
it('should render correctly with minimal props (only activeIndex)', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Stepper Integration Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Stepper Integration', () => {
|
||||
it('should pass steps array with correct structure to Stepper', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert - All step names should be rendered
|
||||
@ -444,7 +361,6 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should render Stepper in centered position', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert - Check for centered positioning classes
|
||||
@ -453,7 +369,6 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should render step dividers between steps', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar({ activeIndex: 0 })
|
||||
|
||||
// Assert - Check for dividers (h-px w-4 bg-divider-deep)
|
||||
@ -462,15 +377,10 @@ describe('TopBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Accessibility Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible back link', () => {
|
||||
// Arrange & Act
|
||||
renderTopBar()
|
||||
|
||||
// Assert
|
||||
const backLink = screen.getByTestId('back-link')
|
||||
expect(backLink).toBeInTheDocument()
|
||||
// Link should have visible text
|
||||
@ -478,7 +388,6 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should have visible arrow icon in back link', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderTopBar()
|
||||
|
||||
// Assert - Arrow icon should be visible
|
||||
@ -488,12 +397,9 @@ describe('TopBar', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Re-render Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Re-render Behavior', () => {
|
||||
it('should update activeIndex on re-render', () => {
|
||||
// Arrange
|
||||
const { rerender, container } = render(<TopBar activeIndex={0} />)
|
||||
|
||||
// Initial check
|
||||
@ -507,21 +413,17 @@ describe('TopBar', () => {
|
||||
})
|
||||
|
||||
it('should update className on re-render', () => {
|
||||
// Arrange
|
||||
const { rerender, container } = render(<TopBar activeIndex={0} className="initial-class" />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('initial-class')
|
||||
|
||||
// Act
|
||||
rerender(<TopBar activeIndex={0} className="updated-class" />)
|
||||
|
||||
// Assert
|
||||
expect(wrapper).toHaveClass('updated-class')
|
||||
expect(wrapper).not.toHaveClass('initial-class')
|
||||
})
|
||||
|
||||
it('should handle multiple rapid re-renders', () => {
|
||||
// Arrange
|
||||
const { rerender, container } = render(<TopBar activeIndex={0} />)
|
||||
|
||||
// Act - Multiple rapid re-renders
|
||||
@ -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