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

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,126 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ModifyExternalRetrievalModal from '../modify-external-retrieval-modal'
vi.mock('@/app/components/base/action-button', () => ({
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
<button data-testid="action-button" onClick={onClick}>{children}</button>
),
}))
vi.mock('@/app/components/base/button', () => ({
default: ({ children, onClick, variant }: { children: React.ReactNode, onClick: () => void, variant?: string }) => (
<button data-testid={variant === 'primary' ? 'save-button' : 'cancel-button'} onClick={onClick}>
{children}
</button>
),
}))
vi.mock('../../external-knowledge-base/create/RetrievalSettings', () => ({
default: ({ topK, scoreThreshold, _scoreThresholdEnabled, onChange }: { topK: number, scoreThreshold: number, _scoreThresholdEnabled: boolean, onChange: (data: Record<string, unknown>) => void }) => (
<div data-testid="retrieval-settings">
<span data-testid="top-k">{topK}</span>
<span data-testid="score-threshold">{scoreThreshold}</span>
<button data-testid="change-top-k" onClick={() => onChange({ top_k: 10 })}>change top k</button>
<button data-testid="change-score" onClick={() => onChange({ score_threshold: 0.9 })}>change score</button>
<button data-testid="change-enabled" onClick={() => onChange({ score_threshold_enabled: true })}>change enabled</button>
</div>
),
}))
describe('ModifyExternalRetrievalModal', () => {
const defaultProps = {
onClose: vi.fn(),
onSave: vi.fn(),
initialTopK: 4,
initialScoreThreshold: 0.5,
initialScoreThresholdEnabled: false,
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should render title', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
expect(screen.getByText('datasetHitTesting.settingTitle')).toBeInTheDocument()
})
it('should render retrieval settings with initial values', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
expect(screen.getByTestId('top-k')).toHaveTextContent('4')
expect(screen.getByTestId('score-threshold')).toHaveTextContent('0.5')
})
it('should call onClose when close button clicked', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('action-button'))
expect(defaultProps.onClose).toHaveBeenCalled()
})
it('should call onClose when cancel button clicked', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('cancel-button'))
expect(defaultProps.onClose).toHaveBeenCalled()
})
it('should call onSave with current values and close when save clicked', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('save-button'))
expect(defaultProps.onSave).toHaveBeenCalledWith({
top_k: 4,
score_threshold: 0.5,
score_threshold_enabled: false,
})
expect(defaultProps.onClose).toHaveBeenCalled()
})
it('should save updated values after settings change', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('change-top-k'))
fireEvent.click(screen.getByTestId('save-button'))
expect(defaultProps.onSave).toHaveBeenCalledWith(
expect.objectContaining({ top_k: 10 }),
)
})
it('should save updated score threshold', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('change-score'))
fireEvent.click(screen.getByTestId('save-button'))
expect(defaultProps.onSave).toHaveBeenCalledWith(
expect.objectContaining({ score_threshold: 0.9 }),
)
})
it('should save updated score threshold enabled', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('change-enabled'))
fireEvent.click(screen.getByTestId('save-button'))
expect(defaultProps.onSave).toHaveBeenCalledWith(
expect.objectContaining({ score_threshold_enabled: true }),
)
})
it('should save multiple updated values at once', () => {
render(<ModifyExternalRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('change-top-k'))
fireEvent.click(screen.getByTestId('change-score'))
fireEvent.click(screen.getByTestId('save-button'))
expect(defaultProps.onSave).toHaveBeenCalledWith(
expect.objectContaining({ top_k: 10, score_threshold: 0.9 }),
)
})
it('should render with different initial values', () => {
const props = {
...defaultProps,
initialTopK: 10,
initialScoreThreshold: 0.8,
initialScoreThresholdEnabled: true,
}
render(<ModifyExternalRetrievalModal {...props} />)
expect(screen.getByTestId('top-k')).toHaveTextContent('10')
expect(screen.getByTestId('score-threshold')).toHaveTextContent('0.8')
})
})

View File

@ -0,0 +1,108 @@
import type { RetrievalConfig } from '@/types/app'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { RETRIEVE_METHOD } from '@/types/app'
import ModifyRetrievalModal from '../modify-retrieval-modal'
vi.mock('@/app/components/base/button', () => ({
default: ({ children, onClick, variant }: { children: React.ReactNode, onClick: () => void, variant?: string }) => (
<button data-testid={variant === 'primary' ? 'save-button' : 'cancel-button'} onClick={onClick}>
{children}
</button>
),
}))
vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({
isReRankModelSelected: vi.fn(() => true),
}))
vi.mock('@/app/components/datasets/common/retrieval-method-config', () => ({
default: ({ value, onChange }: { value: RetrievalConfig, onChange: (v: RetrievalConfig) => void }) => (
<div data-testid="retrieval-method-config">
<span>{value.search_method}</span>
<button data-testid="change-config" onClick={() => onChange({ ...value, search_method: RETRIEVE_METHOD.hybrid })}>change</button>
</div>
),
}))
vi.mock('@/app/components/datasets/common/economical-retrieval-method-config', () => ({
default: () => <div data-testid="economical-config" />,
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: () => ({ data: [] }),
}))
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: () => 'model-name',
}))
vi.mock('@/context/i18n', () => ({
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
}))
vi.mock('../../../base/toast', () => ({
default: { notify: vi.fn() },
}))
vi.mock('../../settings/utils', () => ({
checkShowMultiModalTip: () => false,
}))
describe('ModifyRetrievalModal', () => {
const defaultProps = {
indexMethod: 'high_quality',
value: {
search_method: 'semantic_search',
reranking_enable: false,
reranking_model: {
reranking_provider_name: '',
reranking_model_name: '',
},
} as RetrievalConfig,
isShow: true,
onHide: vi.fn(),
onSave: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should return null when isShow is false', () => {
const { container } = render(<ModifyRetrievalModal {...defaultProps} isShow={false} />)
expect(container.firstChild).toBeNull()
})
it('should render title when isShow is true', () => {
render(<ModifyRetrievalModal {...defaultProps} />)
expect(screen.getByText('datasetSettings.form.retrievalSetting.title')).toBeInTheDocument()
})
it('should render high quality retrieval config for high_quality index', () => {
render(<ModifyRetrievalModal {...defaultProps} />)
expect(screen.getByTestId('retrieval-method-config')).toBeInTheDocument()
})
it('should render economical config for non high_quality index', () => {
render(<ModifyRetrievalModal {...defaultProps} indexMethod="economy" />)
expect(screen.getByTestId('economical-config')).toBeInTheDocument()
})
it('should call onHide when cancel button clicked', () => {
render(<ModifyRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('cancel-button'))
expect(defaultProps.onHide).toHaveBeenCalled()
})
it('should call onSave with retrieval config when save clicked', () => {
render(<ModifyRetrievalModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('save-button'))
expect(defaultProps.onSave).toHaveBeenCalled()
})
it('should render learn more link', () => {
render(<ModifyRetrievalModal {...defaultProps} />)
expect(screen.getByText('datasetSettings.form.retrievalSetting.learnMore')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,97 @@
import type { HitTestingChildChunk } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ChildChunksItem from '../child-chunks-item'
const createChildChunkPayload = (
overrides: Partial<HitTestingChildChunk> = {},
): HitTestingChildChunk => ({
id: 'chunk-1',
content: 'Child chunk content here',
position: 1,
score: 0.75,
...overrides,
})
describe('ChildChunksItem', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for child chunk items
describe('Rendering', () => {
it('should render the position label', () => {
const payload = createChildChunkPayload({ position: 3 })
render(<ChildChunksItem payload={payload} isShowAll={false} />)
expect(screen.getByText(/C-/)).toBeInTheDocument()
expect(screen.getByText(/3/)).toBeInTheDocument()
})
it('should render the score component', () => {
const payload = createChildChunkPayload({ score: 0.88 })
render(<ChildChunksItem payload={payload} isShowAll={false} />)
expect(screen.getByText('0.88')).toBeInTheDocument()
})
it('should render the content text', () => {
const payload = createChildChunkPayload({ content: 'Sample chunk text' })
render(<ChildChunksItem payload={payload} isShowAll={false} />)
expect(screen.getByText('Sample chunk text')).toBeInTheDocument()
})
it('should render with besideChunkName styling on Score', () => {
const payload = createChildChunkPayload({ score: 0.6 })
const { container } = render(
<ChildChunksItem payload={payload} isShowAll={false} />,
)
// Assert - Score with besideChunkName has h-[20.5px] and border-l-0
const scoreEl = container.querySelector('[class*="h-\\[20\\.5px\\]"]')
expect(scoreEl).toBeInTheDocument()
})
})
// Line clamping behavior tests
describe('Line Clamping', () => {
it('should apply line-clamp-2 when isShowAll is false', () => {
const payload = createChildChunkPayload()
const { container } = render(
<ChildChunksItem payload={payload} isShowAll={false} />,
)
const root = container.firstElementChild
expect(root?.className).toContain('line-clamp-2')
})
it('should not apply line-clamp-2 when isShowAll is true', () => {
const payload = createChildChunkPayload()
const { container } = render(
<ChildChunksItem payload={payload} isShowAll={true} />,
)
const root = container.firstElementChild
expect(root?.className).not.toContain('line-clamp-2')
})
})
describe('Edge Cases', () => {
it('should render with score 0 (Score returns null)', () => {
const payload = createChildChunkPayload({ score: 0 })
render(<ChildChunksItem payload={payload} isShowAll={false} />)
// Assert - content still renders, score returns null
expect(screen.getByText('Child chunk content here')).toBeInTheDocument()
expect(screen.queryByText('score')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,137 @@
import type { HitTesting } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ChunkDetailModal from '../chunk-detail-modal'
vi.mock('@/app/components/base/file-uploader/file-type-icon', () => ({
default: () => <span data-testid="file-icon" />,
}))
vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
}))
vi.mock('@/app/components/base/modal', () => ({
default: ({ children, title, onClose }: { children: React.ReactNode, title: string, onClose: () => void }) => (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
<button data-testid="modal-close" onClick={onClose}>close</button>
{children}
</div>
),
}))
vi.mock('../../../common/image-list', () => ({
default: () => <div data-testid="image-list" />,
}))
vi.mock('../../../documents/detail/completed/common/dot', () => ({
default: () => <span data-testid="dot" />,
}))
vi.mock('../../../documents/detail/completed/common/segment-index-tag', () => ({
SegmentIndexTag: ({ positionId }: { positionId: number }) => <span data-testid="segment-index-tag">{positionId}</span>,
}))
vi.mock('../../../documents/detail/completed/common/summary-text', () => ({
default: ({ value }: { value: string }) => <div data-testid="summary-text">{value}</div>,
}))
vi.mock('@/app/components/datasets/documents/detail/completed/common/tag', () => ({
default: ({ text }: { text: string }) => <span data-testid="tag">{text}</span>,
}))
vi.mock('../child-chunks-item', () => ({
default: ({ payload }: { payload: { id: string } }) => <div data-testid="child-chunk">{payload.id}</div>,
}))
vi.mock('../mask', () => ({
default: () => <div data-testid="mask" />,
}))
vi.mock('../score', () => ({
default: ({ value }: { value: number }) => <span data-testid="score">{value}</span>,
}))
const makePayload = (overrides: Record<string, unknown> = {}): HitTesting => {
const segmentOverrides = (overrides.segment ?? {}) as Record<string, unknown>
const segment = {
position: 1,
content: 'chunk content',
sign_content: '',
keywords: [],
document: { name: 'file.pdf' },
answer: '',
word_count: 100,
...segmentOverrides,
}
return {
segment,
content: segment,
score: 0.85,
tsne_position: { x: 0, y: 0 },
child_chunks: (overrides.child_chunks ?? []) as HitTesting['child_chunks'],
files: (overrides.files ?? []) as HitTesting['files'],
summary: (overrides.summary ?? '') as string,
} as unknown as HitTesting
}
describe('ChunkDetailModal', () => {
const onHide = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render modal with title', () => {
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
expect(screen.getByTestId('modal-title')).toHaveTextContent('chunkDetail')
})
it('should render segment index tag and score', () => {
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
expect(screen.getByTestId('segment-index-tag')).toHaveTextContent('1')
expect(screen.getByTestId('score')).toHaveTextContent('0.85')
})
it('should render markdown content', () => {
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
expect(screen.getByTestId('markdown')).toHaveTextContent('chunk content')
})
it('should render QA content when answer exists', () => {
const payload = makePayload({
segment: { answer: 'answer text', content: 'question text' },
})
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
expect(screen.getByText('question text')).toBeInTheDocument()
expect(screen.getByText('answer text')).toBeInTheDocument()
})
it('should render keywords when present and not parent-child', () => {
const payload = makePayload({
segment: { keywords: ['k1', 'k2'] },
})
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
expect(screen.getAllByTestId('tag')).toHaveLength(2)
})
it('should render child chunks section for parent-child retrieval', () => {
const payload = makePayload({
child_chunks: [{ id: 'c1' }, { id: 'c2' }],
})
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
expect(screen.getAllByTestId('child-chunk')).toHaveLength(2)
})
it('should render summary text when summary exists', () => {
const payload = makePayload({ summary: 'test summary' })
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
expect(screen.getByTestId('summary-text')).toHaveTextContent('test summary')
})
it('should render mask overlay', () => {
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
expect(screen.getByTestId('mask')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,33 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import EmptyRecords from '../empty-records'
describe('EmptyRecords', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for the empty state component
describe('Rendering', () => {
it('should render the "no recent" tip text', () => {
render(<EmptyRecords />)
expect(screen.getByText(/noRecentTip/i)).toBeInTheDocument()
})
it('should render the history icon', () => {
const { container } = render(<EmptyRecords />)
const svg = container.querySelector('svg')
expect(svg).toBeInTheDocument()
})
it('should render inside a styled container', () => {
const { container } = render(<EmptyRecords />)
const wrapper = container.firstElementChild
expect(wrapper?.className).toContain('rounded-2xl')
expect(wrapper?.className).toContain('bg-workflow-process-bg')
})
})
})

View File

@ -0,0 +1,33 @@
import { render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Mask from '../mask'
describe('Mask', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for the gradient overlay component
describe('Rendering', () => {
it('should render a gradient overlay div', () => {
const { container } = render(<Mask />)
const div = container.firstElementChild
expect(div).toBeInTheDocument()
expect(div?.className).toContain('h-12')
expect(div?.className).toContain('bg-gradient-to-b')
})
it('should apply custom className', () => {
const { container } = render(<Mask className="custom-mask" />)
expect(container.firstElementChild?.className).toContain('custom-mask')
})
it('should render without custom className', () => {
const { container } = render(<Mask />)
expect(container.firstElementChild).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,95 @@
import type { HitTestingRecord } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Records from '../records'
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: (ts: number, _fmt: string) => `time-${ts}`,
}),
}))
vi.mock('../../../common/image-list', () => ({
default: () => <div data-testid="image-list" />,
}))
const makeRecord = (id: string, source: string, created_at: number, content = 'query text') => ({
id,
source,
created_at,
queries: [{ content, content_type: 'text_query', file_info: null }],
}) as unknown as HitTestingRecord
describe('Records', () => {
const mockOnClick = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render table headers', () => {
render(<Records records={[]} onClickRecord={mockOnClick} />)
expect(screen.getByText('datasetHitTesting.table.header.queryContent')).toBeInTheDocument()
expect(screen.getByText('datasetHitTesting.table.header.source')).toBeInTheDocument()
expect(screen.getByText('datasetHitTesting.table.header.time')).toBeInTheDocument()
})
it('should render records', () => {
const records = [
makeRecord('1', 'app', 1000),
makeRecord('2', 'hit_testing', 2000),
]
render(<Records records={records} onClickRecord={mockOnClick} />)
expect(screen.getAllByText('query text')).toHaveLength(2)
})
it('should call onClickRecord when row clicked', () => {
const records = [makeRecord('1', 'app', 1000)]
render(<Records records={records} onClickRecord={mockOnClick} />)
fireEvent.click(screen.getByText('query text'))
expect(mockOnClick).toHaveBeenCalledWith(records[0])
})
it('should sort records by time descending by default', () => {
const records = [
makeRecord('1', 'app', 1000, 'early'),
makeRecord('2', 'app', 3000, 'late'),
makeRecord('3', 'app', 2000, 'mid'),
]
render(<Records records={records} onClickRecord={mockOnClick} />)
const rows = screen.getAllByRole('row').slice(1) // skip header
expect(rows[0]).toHaveTextContent('late')
expect(rows[1]).toHaveTextContent('mid')
expect(rows[2]).toHaveTextContent('early')
})
it('should toggle sort order on time header click', () => {
const records = [
makeRecord('1', 'app', 1000, 'early'),
makeRecord('2', 'app', 3000, 'late'),
]
render(<Records records={records} onClickRecord={mockOnClick} />)
// Default: desc, so late first
let rows = screen.getAllByRole('row').slice(1)
expect(rows[0]).toHaveTextContent('late')
fireEvent.click(screen.getByText('datasetHitTesting.table.header.time'))
rows = screen.getAllByRole('row').slice(1)
expect(rows[0]).toHaveTextContent('early')
})
it('should render image list for image queries', () => {
const records = [{
id: '1',
source: 'app',
created_at: 1000,
queries: [
{ content: '', content_type: 'text_query', file_info: null },
{ content: '', content_type: 'image_query', file_info: { name: 'img.png', mime_type: 'image/png', source_url: 'url', size: 100, extension: 'png' } },
],
}] as unknown as HitTestingRecord[]
render(<Records records={records} onClickRecord={mockOnClick} />)
expect(screen.getByTestId('image-list')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,173 @@
import type { ExternalKnowledgeBaseHitTesting } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ResultItemExternal from '../result-item-external'
let mockIsShowDetailModal = false
const mockShowDetailModal = vi.fn(() => {
mockIsShowDetailModal = true
})
const mockHideDetailModal = vi.fn(() => {
mockIsShowDetailModal = false
})
// Mock useBoolean: required because tests control modal state externally
// (setting mockIsShowDetailModal before render) and verify mock fn calls.
vi.mock('ahooks', () => ({
useBoolean: (_initial: boolean) => {
return [
mockIsShowDetailModal,
{
setTrue: mockShowDetailModal,
setFalse: mockHideDetailModal,
toggle: vi.fn(),
set: vi.fn(),
},
]
},
}))
const createExternalPayload = (
overrides: Partial<ExternalKnowledgeBaseHitTesting> = {},
): ExternalKnowledgeBaseHitTesting => ({
content: 'This is the chunk content for testing.',
title: 'Test Document Title',
score: 0.85,
metadata: {
'x-amz-bedrock-kb-source-uri': 's3://bucket/key',
'x-amz-bedrock-kb-data-source-id': 'ds-123',
},
...overrides,
})
describe('ResultItemExternal', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsShowDetailModal = false
})
// Rendering tests for the external result item card
describe('Rendering', () => {
it('should render the content text', () => {
const payload = createExternalPayload({ content: 'External result content' })
render(<ResultItemExternal payload={payload} positionId={1} />)
expect(screen.getByText('External result content')).toBeInTheDocument()
})
it('should render the meta info with position and score', () => {
const payload = createExternalPayload({ score: 0.92 })
render(<ResultItemExternal payload={payload} positionId={5} />)
expect(screen.getByText('Chunk-05')).toBeInTheDocument()
expect(screen.getByText('0.92')).toBeInTheDocument()
})
it('should render the footer with document title', () => {
const payload = createExternalPayload({ title: 'Knowledge Base Doc' })
render(<ResultItemExternal payload={payload} positionId={1} />)
expect(screen.getByText('Knowledge Base Doc')).toBeInTheDocument()
})
it('should render the word count from content length', () => {
const content = 'Hello World' // 11 chars
const payload = createExternalPayload({ content })
render(<ResultItemExternal payload={payload} positionId={1} />)
expect(screen.getByText(/11/)).toBeInTheDocument()
})
})
// Detail modal tests
describe('Detail Modal', () => {
it('should not render modal by default', () => {
const payload = createExternalPayload()
render(<ResultItemExternal payload={payload} positionId={1} />)
expect(screen.queryByText(/chunkDetail/i)).not.toBeInTheDocument()
})
it('should call showDetailModal when card is clicked', () => {
const payload = createExternalPayload()
mockIsShowDetailModal = false
render(<ResultItemExternal payload={payload} positionId={1} />)
// Act - click the card to open modal
const card = screen.getByText(payload.content).closest('.cursor-pointer') as HTMLElement
fireEvent.click(card)
// Assert - showDetailModal (setTrue) was invoked
expect(mockShowDetailModal).toHaveBeenCalled()
})
it('should render modal content when isShowDetailModal is true', () => {
// Arrange - modal is already open
const payload = createExternalPayload()
mockIsShowDetailModal = true
render(<ResultItemExternal payload={payload} positionId={1} />)
// Assert - modal title should appear
expect(screen.getByText(/chunkDetail/i)).toBeInTheDocument()
})
it('should render full content in the modal', () => {
const payload = createExternalPayload({ content: 'Full modal content text' })
mockIsShowDetailModal = true
render(<ResultItemExternal payload={payload} positionId={1} />)
// Assert - content appears both in card and modal
const contentElements = screen.getAllByText('Full modal content text')
expect(contentElements.length).toBeGreaterThanOrEqual(2)
})
it('should render meta info in the modal', () => {
const payload = createExternalPayload({ score: 0.77 })
mockIsShowDetailModal = true
render(<ResultItemExternal payload={payload} positionId={3} />)
// Assert - meta appears in both card and modal
const chunkTags = screen.getAllByText('Chunk-03')
expect(chunkTags.length).toBe(2)
const scores = screen.getAllByText('0.77')
expect(scores.length).toBe(2)
})
})
describe('Edge Cases', () => {
it('should render with empty content', () => {
const payload = createExternalPayload({ content: '' })
render(<ResultItemExternal payload={payload} positionId={1} />)
// Assert - component still renders
expect(screen.getByText('Test Document Title')).toBeInTheDocument()
})
it('should render with score of 0 (Score returns null)', () => {
const payload = createExternalPayload({ score: 0 })
render(<ResultItemExternal payload={payload} positionId={1} />)
// Assert - no score displayed
expect(screen.queryByText('score')).not.toBeInTheDocument()
})
it('should handle large positionId values', () => {
const payload = createExternalPayload()
render(<ResultItemExternal payload={payload} positionId={999} />)
expect(screen.getByText('Chunk-999')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,70 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
import ResultItemFooter from '../result-item-footer'
describe('ResultItemFooter', () => {
const mockShowDetailModal = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for the result item footer
describe('Rendering', () => {
it('should render the document title', () => {
render(
<ResultItemFooter
docType={FileAppearanceTypeEnum.document}
docTitle="My Document.pdf"
showDetailModal={mockShowDetailModal}
/>,
)
expect(screen.getByText('My Document.pdf')).toBeInTheDocument()
})
it('should render the "open" button text', () => {
render(
<ResultItemFooter
docType={FileAppearanceTypeEnum.pdf}
docTitle="File.pdf"
showDetailModal={mockShowDetailModal}
/>,
)
expect(screen.getByText(/open/i)).toBeInTheDocument()
})
it('should render the file icon', () => {
const { container } = render(
<ResultItemFooter
docType={FileAppearanceTypeEnum.document}
docTitle="File.txt"
showDetailModal={mockShowDetailModal}
/>,
)
const icon = container.querySelector('svg')
expect(icon).toBeInTheDocument()
})
})
// User interaction tests
describe('User Interactions', () => {
it('should call showDetailModal when open button is clicked', () => {
render(
<ResultItemFooter
docType={FileAppearanceTypeEnum.document}
docTitle="Doc"
showDetailModal={mockShowDetailModal}
/>,
)
const openButton = screen.getByText(/open/i)
fireEvent.click(openButton)
expect(mockShowDetailModal).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -0,0 +1,80 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ResultItemMeta from '../result-item-meta'
describe('ResultItemMeta', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for the result item meta component
describe('Rendering', () => {
it('should render the segment index tag with prefix and position', () => {
render(
<ResultItemMeta
labelPrefix="Chunk"
positionId={3}
wordCount={150}
score={0.9}
/>,
)
expect(screen.getByText('Chunk-03')).toBeInTheDocument()
})
it('should render the word count', () => {
render(
<ResultItemMeta
labelPrefix="Chunk"
positionId={1}
wordCount={250}
score={0.8}
/>,
)
expect(screen.getByText(/250/)).toBeInTheDocument()
expect(screen.getByText(/characters/i)).toBeInTheDocument()
})
it('should render the score component', () => {
render(
<ResultItemMeta
labelPrefix="Chunk"
positionId={1}
wordCount={100}
score={0.75}
/>,
)
expect(screen.getByText('0.75')).toBeInTheDocument()
expect(screen.getByText('score')).toBeInTheDocument()
})
it('should apply custom className', () => {
const { container } = render(
<ResultItemMeta
className="custom-meta"
labelPrefix="Chunk"
positionId={1}
wordCount={100}
score={0.5}
/>,
)
expect(container.firstElementChild?.className).toContain('custom-meta')
})
it('should render dot separator', () => {
render(
<ResultItemMeta
labelPrefix="Chunk"
positionId={1}
wordCount={100}
score={0.5}
/>,
)
expect(screen.getByText('·')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,144 @@
import type { HitTesting } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ResultItem from '../result-item'
vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
}))
vi.mock('../../../common/image-list', () => ({
default: () => <div data-testid="image-list" />,
}))
vi.mock('../child-chunks-item', () => ({
default: ({ payload }: { payload: { id: string } }) => <div data-testid="child-chunk">{payload.id}</div>,
}))
vi.mock('../chunk-detail-modal', () => ({
default: () => <div data-testid="chunk-detail-modal" />,
}))
vi.mock('../result-item-footer', () => ({
default: ({ docTitle }: { docTitle: string }) => <div data-testid="result-item-footer">{docTitle}</div>,
}))
vi.mock('../result-item-meta', () => ({
default: ({ positionId }: { positionId: number }) => <div data-testid="result-item-meta">{positionId}</div>,
}))
vi.mock('@/app/components/datasets/documents/detail/completed/common/summary-label', () => ({
default: ({ summary }: { summary: string }) => <div data-testid="summary-label">{summary}</div>,
}))
vi.mock('@/app/components/datasets/documents/detail/completed/common/tag', () => ({
default: ({ text }: { text: string }) => <span data-testid="tag">{text}</span>,
}))
vi.mock('@/app/components/datasets/hit-testing/utils/extension-to-file-type', () => ({
extensionToFileType: () => 'pdf',
}))
const makePayload = (overrides: Record<string, unknown> = {}): HitTesting => {
const segmentOverrides = (overrides.segment ?? {}) as Record<string, unknown>
const segment = {
position: 1,
word_count: 100,
content: 'test content',
sign_content: '',
keywords: [],
document: { name: 'file.pdf' },
answer: '',
...segmentOverrides,
}
return {
segment,
content: segment,
score: 0.95,
tsne_position: { x: 0, y: 0 },
child_chunks: (overrides.child_chunks ?? []) as HitTesting['child_chunks'],
files: (overrides.files ?? []) as HitTesting['files'],
summary: (overrides.summary ?? '') as string,
} as unknown as HitTesting
}
describe('ResultItem', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should render meta, content, and footer', () => {
render(<ResultItem payload={makePayload()} />)
expect(screen.getByTestId('result-item-meta')).toHaveTextContent('1')
expect(screen.getByTestId('markdown')).toHaveTextContent('test content')
expect(screen.getByTestId('result-item-footer')).toHaveTextContent('file.pdf')
})
it('should render keywords when no child_chunks', () => {
const payload = makePayload({
segment: { keywords: ['key1', 'key2'] },
})
render(<ResultItem payload={payload} />)
expect(screen.getAllByTestId('tag')).toHaveLength(2)
})
it('should render child chunks when present', () => {
const payload = makePayload({
child_chunks: [{ id: 'c1' }, { id: 'c2' }],
})
render(<ResultItem payload={payload} />)
expect(screen.getAllByTestId('child-chunk')).toHaveLength(2)
})
it('should render summary label when summary exists', () => {
const payload = makePayload({ summary: 'test summary' })
render(<ResultItem payload={payload} />)
expect(screen.getByTestId('summary-label')).toHaveTextContent('test summary')
})
it('should show chunk detail modal on click', () => {
render(<ResultItem payload={makePayload()} />)
fireEvent.click(screen.getByTestId('markdown'))
expect(screen.getByTestId('chunk-detail-modal')).toBeInTheDocument()
})
it('should render images when files exist', () => {
const payload = makePayload({
files: [{ name: 'img.png', mime_type: 'image/png', source_url: 'url', size: 100, extension: 'png' }],
})
render(<ResultItem payload={payload} />)
expect(screen.getByTestId('image-list')).toBeInTheDocument()
})
it('should not render keywords when child_chunks are present', () => {
const payload = makePayload({
segment: { keywords: ['k1'] },
child_chunks: [{ id: 'c1' }],
})
render(<ResultItem payload={payload} />)
expect(screen.queryByTestId('tag')).not.toBeInTheDocument()
})
it('should not render keywords section when keywords array is empty', () => {
const payload = makePayload({
segment: { keywords: [] },
})
render(<ResultItem payload={payload} />)
expect(screen.queryByTestId('tag')).not.toBeInTheDocument()
})
it('should toggle child chunks fold state', async () => {
const payload = makePayload({
child_chunks: [{ id: 'c1' }],
})
render(<ResultItem payload={payload} />)
expect(screen.getByTestId('child-chunk')).toBeInTheDocument()
const header = screen.getByText(/hitChunks/i)
fireEvent.click(header.closest('div')!)
await waitFor(() => {
expect(screen.queryByTestId('child-chunk')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,92 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Score from '../score'
describe('Score', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for the score display component
describe('Rendering', () => {
it('should render score value with toFixed(2)', () => {
render(<Score value={0.85} />)
expect(screen.getByText('0.85')).toBeInTheDocument()
expect(screen.getByText('score')).toBeInTheDocument()
})
it('should render score progress bar with correct width', () => {
const { container } = render(<Score value={0.75} />)
const progressBar = container.querySelector('[style]')
expect(progressBar).toHaveStyle({ width: '75%' })
})
it('should render with besideChunkName styling', () => {
const { container } = render(<Score value={0.5} besideChunkName />)
const root = container.firstElementChild
expect(root?.className).toContain('h-[20.5px]')
expect(root?.className).toContain('border-l-0')
})
it('should render with default styling when besideChunkName is false', () => {
const { container } = render(<Score value={0.5} />)
const root = container.firstElementChild
expect(root?.className).toContain('h-[20px]')
expect(root?.className).toContain('rounded-md')
})
it('should remove right border when value is exactly 1', () => {
const { container } = render(<Score value={1} />)
const progressBar = container.querySelector('[style]')
expect(progressBar?.className).toContain('border-r-0')
expect(progressBar).toHaveStyle({ width: '100%' })
})
it('should show right border when value is less than 1', () => {
const { container } = render(<Score value={0.5} />)
const progressBar = container.querySelector('[style]')
expect(progressBar?.className).not.toContain('border-r-0')
})
})
// Null return tests for edge cases
describe('Returns null', () => {
it('should return null when value is null', () => {
const { container } = render(<Score value={null} />)
expect(container.innerHTML).toBe('')
})
it('should return null when value is 0', () => {
const { container } = render(<Score value={0} />)
expect(container.innerHTML).toBe('')
})
it('should return null when value is NaN', () => {
const { container } = render(<Score value={Number.NaN} />)
expect(container.innerHTML).toBe('')
})
})
describe('Edge Cases', () => {
it('should render very small score values', () => {
render(<Score value={0.01} />)
expect(screen.getByText('0.01')).toBeInTheDocument()
})
it('should render score with many decimals truncated to 2', () => {
render(<Score value={0.123456} />)
expect(screen.getByText('0.12')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,418 @@
import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types'
import type { Query } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import QueryInput from '../index'
// Capture onChange callback so tests can trigger handleImageChange
let capturedOnChange: ((files: FileEntity[]) => void) | null = null
vi.mock('@/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing', () => ({
default: ({ textArea, actionButton, onChange }: { textArea: React.ReactNode, actionButton: React.ReactNode, onChange?: (files: FileEntity[]) => void }) => {
capturedOnChange = onChange ?? null
return (
<div data-testid="image-uploader">
{textArea}
{actionButton}
</div>
)
},
}))
vi.mock('@/app/components/datasets/common/retrieval-method-info', () => ({
getIcon: () => '/test-icon.png',
}))
// Capture onSave callback for external retrieval modal
let _capturedModalOnSave: ((data: { top_k: number, score_threshold: number, score_threshold_enabled: boolean }) => void) | null = null
vi.mock('@/app/components/datasets/hit-testing/modify-external-retrieval-modal', () => ({
default: ({ onSave, onClose }: { onSave: (data: { top_k: number, score_threshold: number, score_threshold_enabled: boolean }) => void, onClose: () => void }) => {
_capturedModalOnSave = onSave
return (
<div data-testid="external-retrieval-modal">
<button data-testid="modal-save" onClick={() => onSave({ top_k: 10, score_threshold: 0.8, score_threshold_enabled: true })}>Save</button>
<button data-testid="modal-close" onClick={onClose}>Close</button>
</div>
)
},
}))
// Capture handleTextChange callback
let _capturedHandleTextChange: ((e: React.ChangeEvent<HTMLTextAreaElement>) => void) | null = null
vi.mock('../textarea', () => ({
default: ({ text, handleTextChange }: { text: string, handleTextChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void }) => {
_capturedHandleTextChange = handleTextChange
return <textarea data-testid="textarea" defaultValue={text} onChange={handleTextChange} />
},
}))
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: () => false,
}))
describe('QueryInput', () => {
// Re-create per test to avoid cross-test mutation (handleTextChange mutates query objects)
const makeDefaultProps = () => ({
onUpdateList: vi.fn(),
setHitResult: vi.fn(),
setExternalHitResult: vi.fn(),
loading: false,
queries: [{ content: 'test query', content_type: 'text_query', file_info: null }] satisfies Query[],
setQueries: vi.fn(),
isExternal: false,
onClickRetrievalMethod: vi.fn(),
retrievalConfig: { search_method: 'semantic_search' } as RetrievalConfig,
isEconomy: false,
hitTestingMutation: vi.fn(),
externalKnowledgeBaseHitTestingMutation: vi.fn(),
})
let defaultProps: ReturnType<typeof makeDefaultProps>
beforeEach(() => {
vi.clearAllMocks()
defaultProps = makeDefaultProps()
capturedOnChange = null
_capturedModalOnSave = null
_capturedHandleTextChange = null
})
it('should render title', () => {
render(<QueryInput {...defaultProps} />)
expect(screen.getByText('datasetHitTesting.input.title')).toBeInTheDocument()
})
it('should render textarea with query text', () => {
render(<QueryInput {...defaultProps} />)
expect(screen.getByTestId('textarea')).toBeInTheDocument()
})
it('should render submit button', () => {
render(<QueryInput {...defaultProps} />)
expect(screen.getByRole('button', { name: /input\.testing/ })).toBeInTheDocument()
})
it('should disable submit button when text is empty', () => {
const props = {
...defaultProps,
queries: [{ content: '', content_type: 'text_query', file_info: null }] satisfies Query[],
}
render(<QueryInput {...props} />)
expect(screen.getByRole('button', { name: /input\.testing/ })).toBeDisabled()
})
it('should render retrieval method for non-external mode', () => {
render(<QueryInput {...defaultProps} />)
expect(screen.getByText('dataset.retrieval.semantic_search.title')).toBeInTheDocument()
})
it('should render settings button for external mode', () => {
render(<QueryInput {...defaultProps} isExternal={true} />)
expect(screen.getByText('datasetHitTesting.settingTitle')).toBeInTheDocument()
})
it('should disable submit button when text exceeds 200 characters', () => {
const props = {
...defaultProps,
queries: [{ content: 'a'.repeat(201), content_type: 'text_query', file_info: null }] satisfies Query[],
}
render(<QueryInput {...props} />)
expect(screen.getByRole('button', { name: /input\.testing/ })).toBeDisabled()
})
it('should show loading state on submit button when loading', () => {
render(<QueryInput {...defaultProps} loading={true} />)
const submitButton = screen.getByRole('button', { name: /input\.testing/ })
// The real Button component does not disable on loading; it shows a spinner
expect(submitButton).toBeInTheDocument()
expect(submitButton.querySelector('[role="status"]')).toBeInTheDocument()
})
// Cover line 83: images useMemo with image_query data
describe('Image Queries', () => {
it('should parse image_query entries from queries', () => {
const queries: Query[] = [
{ content: 'test', content_type: 'text_query', file_info: null },
{
content: 'https://img.example.com/1.png',
content_type: 'image_query',
file_info: { id: 'img-1', name: 'photo.png', size: 1024, mime_type: 'image/png', extension: 'png', source_url: 'https://img.example.com/1.png' },
},
]
render(<QueryInput {...defaultProps} queries={queries} />)
// Submit should be enabled since we have text + uploaded image
expect(screen.getByRole('button', { name: /input\.testing/ })).not.toBeDisabled()
})
})
// Cover lines 106-107: handleSaveExternalRetrievalSettings
describe('External Retrieval Settings', () => {
it('should open and close external retrieval modal', () => {
render(<QueryInput {...defaultProps} isExternal={true} />)
// Click settings button to open modal
fireEvent.click(screen.getByRole('button', { name: /settingTitle/ }))
expect(screen.getByTestId('external-retrieval-modal')).toBeInTheDocument()
// Close modal
fireEvent.click(screen.getByTestId('modal-close'))
expect(screen.queryByTestId('external-retrieval-modal')).not.toBeInTheDocument()
})
it('should save external retrieval settings and close modal', () => {
render(<QueryInput {...defaultProps} isExternal={true} />)
// Open modal
fireEvent.click(screen.getByRole('button', { name: /settingTitle/ }))
expect(screen.getByTestId('external-retrieval-modal')).toBeInTheDocument()
// Save settings
fireEvent.click(screen.getByTestId('modal-save'))
expect(screen.queryByTestId('external-retrieval-modal')).not.toBeInTheDocument()
})
})
// Cover line 121: handleTextChange when textQuery already exists
describe('Text Change Handling', () => {
it('should update existing text query on text change', () => {
render(<QueryInput {...defaultProps} />)
const textarea = screen.getByTestId('textarea')
fireEvent.change(textarea, { target: { value: 'updated text' } })
expect(defaultProps.setQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ content: 'updated text', content_type: 'text_query' }),
]),
)
})
it('should create new text query when none exists', () => {
render(<QueryInput {...defaultProps} queries={[]} />)
const textarea = screen.getByTestId('textarea')
fireEvent.change(textarea, { target: { value: 'new text' } })
expect(defaultProps.setQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ content: 'new text', content_type: 'text_query' }),
]),
)
})
})
// Cover lines 127-143: handleImageChange
describe('Image Change Handling', () => {
it('should update queries when images change', () => {
render(<QueryInput {...defaultProps} />)
const files: FileEntity[] = [{
id: 'f-1',
name: 'pic.jpg',
size: 2048,
mimeType: 'image/jpeg',
extension: 'jpg',
sourceUrl: 'https://img.example.com/pic.jpg',
uploadedId: 'uploaded-1',
progress: 100,
}]
capturedOnChange?.(files)
expect(defaultProps.setQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ content_type: 'text_query' }),
expect.objectContaining({
content: 'https://img.example.com/pic.jpg',
content_type: 'image_query',
file_info: expect.objectContaining({ id: 'uploaded-1', name: 'pic.jpg' }),
}),
]),
)
})
it('should handle files with missing sourceUrl and uploadedId', () => {
render(<QueryInput {...defaultProps} />)
const files: FileEntity[] = [{
id: 'f-2',
name: 'no-url.jpg',
size: 512,
mimeType: 'image/jpeg',
extension: 'jpg',
progress: 100,
// sourceUrl and uploadedId are undefined
}]
capturedOnChange?.(files)
expect(defaultProps.setQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
content: '',
content_type: 'image_query',
file_info: expect.objectContaining({ id: '', source_url: '' }),
}),
]),
)
})
it('should replace all existing image queries with new ones', () => {
const queries: Query[] = [
{ content: 'text', content_type: 'text_query', file_info: null },
{ content: 'old-img', content_type: 'image_query', file_info: { id: 'old', name: 'old.png', size: 100, mime_type: 'image/png', extension: 'png', source_url: '' } },
]
render(<QueryInput {...defaultProps} queries={queries} />)
capturedOnChange?.([])
// Should keep text query but remove all image queries
expect(defaultProps.setQueries).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ content_type: 'text_query' }),
]),
)
// Should not contain image_query
const calledWith = defaultProps.setQueries.mock.calls[0][0] as Query[]
expect(calledWith.filter(q => q.content_type === 'image_query')).toHaveLength(0)
})
})
// Cover lines 146-162: onSubmit (hit testing mutation)
describe('Submit Handlers', () => {
it('should call hitTestingMutation on submit for non-external mode', async () => {
const mockMutation = vi.fn(async (_req, opts) => {
const response = { query: { content: '', tsne_position: { x: 0, y: 0 } }, records: [] }
opts?.onSuccess?.(response)
return response
})
render(<QueryInput {...defaultProps} hitTestingMutation={mockMutation} />)
fireEvent.click(screen.getByRole('button', { name: /input\.testing/ }))
await waitFor(() => {
expect(mockMutation).toHaveBeenCalledWith(
expect.objectContaining({
query: 'test query',
retrieval_model: expect.objectContaining({ search_method: 'semantic_search' }),
}),
expect.objectContaining({ onSuccess: expect.any(Function) }),
)
})
expect(defaultProps.setHitResult).toHaveBeenCalled()
expect(defaultProps.onUpdateList).toHaveBeenCalled()
})
it('should call onSubmit callback after successful hit testing', async () => {
const mockOnSubmit = vi.fn()
const mockMutation = vi.fn(async (_req, opts) => {
const response = { query: { content: '', tsne_position: { x: 0, y: 0 } }, records: [] }
opts?.onSuccess?.(response)
return response
})
render(<QueryInput {...defaultProps} hitTestingMutation={mockMutation} onSubmit={mockOnSubmit} />)
fireEvent.click(screen.getByRole('button', { name: /input\.testing/ }))
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalled()
})
})
it('should use keywordSearch when isEconomy is true', async () => {
const mockResponse = { query: { content: '', tsne_position: { x: 0, y: 0 } }, records: [] }
const mockMutation = vi.fn(async (_req, opts) => {
opts?.onSuccess?.(mockResponse)
return mockResponse
})
render(<QueryInput {...defaultProps} hitTestingMutation={mockMutation} isEconomy={true} />)
fireEvent.click(screen.getByRole('button', { name: /input\.testing/ }))
await waitFor(() => {
expect(mockMutation).toHaveBeenCalledWith(
expect.objectContaining({
retrieval_model: expect.objectContaining({ search_method: 'keyword_search' }),
}),
expect.anything(),
)
})
})
// Cover lines 164-178: externalRetrievalTestingOnSubmit
it('should call externalKnowledgeBaseHitTestingMutation for external mode', async () => {
const mockExternalMutation = vi.fn(async (_req, opts) => {
const response = { query: { content: '' }, records: [] }
opts?.onSuccess?.(response)
return response
})
render(<QueryInput {...defaultProps} isExternal={true} externalKnowledgeBaseHitTestingMutation={mockExternalMutation} />)
fireEvent.click(screen.getByRole('button', { name: /input\.testing/ }))
await waitFor(() => {
expect(mockExternalMutation).toHaveBeenCalledWith(
expect.objectContaining({
query: 'test query',
external_retrieval_model: expect.objectContaining({
top_k: 4,
score_threshold: 0.5,
score_threshold_enabled: false,
}),
}),
expect.objectContaining({ onSuccess: expect.any(Function) }),
)
})
expect(defaultProps.setExternalHitResult).toHaveBeenCalled()
expect(defaultProps.onUpdateList).toHaveBeenCalled()
})
it('should include image attachment_ids in submit request', async () => {
const queries: Query[] = [
{ content: 'test', content_type: 'text_query', file_info: null },
{ content: 'img-url', content_type: 'image_query', file_info: { id: 'img-id', name: 'pic.png', size: 100, mime_type: 'image/png', extension: 'png', source_url: 'img-url' } },
]
const mockResponse = { query: { content: '', tsne_position: { x: 0, y: 0 } }, records: [] }
const mockMutation = vi.fn(async (_req, opts) => {
opts?.onSuccess?.(mockResponse)
return mockResponse
})
render(<QueryInput {...defaultProps} queries={queries} hitTestingMutation={mockMutation} />)
fireEvent.click(screen.getByRole('button', { name: /input\.testing/ }))
await waitFor(() => {
expect(mockMutation).toHaveBeenCalledWith(
expect.objectContaining({
// uploadedId is mapped from file_info.id
attachment_ids: expect.arrayContaining(['img-id']),
}),
expect.anything(),
)
})
})
})
// Cover lines 217-238: retrieval method click handler
describe('Retrieval Method', () => {
it('should call onClickRetrievalMethod when retrieval method is clicked', () => {
render(<QueryInput {...defaultProps} />)
fireEvent.click(screen.getByText('dataset.retrieval.semantic_search.title'))
expect(defaultProps.onClickRetrievalMethod).toHaveBeenCalled()
})
it('should show keyword_search when isEconomy is true', () => {
render(<QueryInput {...defaultProps} isEconomy={true} />)
expect(screen.getByText('dataset.retrieval.keyword_search.title')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,120 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Textarea from '../textarea'
describe('Textarea', () => {
const mockHandleTextChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests for the textarea with character count
describe('Rendering', () => {
it('should render a textarea element', () => {
render(<Textarea text="" handleTextChange={mockHandleTextChange} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should display the current text', () => {
render(<Textarea text="Hello world" handleTextChange={mockHandleTextChange} />)
expect(screen.getByRole('textbox')).toHaveValue('Hello world')
})
it('should show character count', () => {
render(<Textarea text="Hello" handleTextChange={mockHandleTextChange} />)
expect(screen.getByText('5/200')).toBeInTheDocument()
})
it('should show 0/200 for empty text', () => {
render(<Textarea text="" handleTextChange={mockHandleTextChange} />)
expect(screen.getByText('0/200')).toBeInTheDocument()
})
it('should render placeholder text', () => {
render(<Textarea text="" handleTextChange={mockHandleTextChange} />)
expect(screen.getByRole('textbox')).toHaveAttribute('placeholder')
})
})
// Warning state tests for exceeding character limit
describe('Warning state (>200 chars)', () => {
it('should apply warning border when text exceeds 200 characters', () => {
const longText = 'A'.repeat(201)
const { container } = render(
<Textarea text={longText} handleTextChange={mockHandleTextChange} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.className).toContain('border-state-destructive-active')
})
it('should not apply warning border when text is at 200 characters', () => {
const text200 = 'A'.repeat(200)
const { container } = render(
<Textarea text={text200} handleTextChange={mockHandleTextChange} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.className).not.toContain('border-state-destructive-active')
})
it('should not apply warning border when text is under 200 characters', () => {
const { container } = render(
<Textarea text="Short text" handleTextChange={mockHandleTextChange} />,
)
const wrapper = container.firstElementChild
expect(wrapper?.className).not.toContain('border-state-destructive-active')
})
it('should show warning count with red styling when over 200 chars', () => {
const longText = 'B'.repeat(250)
render(<Textarea text={longText} handleTextChange={mockHandleTextChange} />)
const countElement = screen.getByText('250/200')
expect(countElement.className).toContain('text-util-colors-red-red-600')
})
it('should show normal count styling when at or under 200 chars', () => {
render(<Textarea text="Short" handleTextChange={mockHandleTextChange} />)
const countElement = screen.getByText('5/200')
expect(countElement.className).toContain('text-text-tertiary')
})
it('should show red corner icon when over 200 chars', () => {
const longText = 'C'.repeat(201)
const { container } = render(
<Textarea text={longText} handleTextChange={mockHandleTextChange} />,
)
// Assert - Corner icon should have red class
const cornerWrapper = container.querySelector('.right-0.top-0')
const cornerSvg = cornerWrapper?.querySelector('svg')
expect(cornerSvg?.className.baseVal || cornerSvg?.getAttribute('class')).toContain('text-util-colors-red-red-100')
})
})
// User interaction tests
describe('User Interactions', () => {
it('should call handleTextChange when text is entered', () => {
render(<Textarea text="" handleTextChange={mockHandleTextChange} />)
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'New text' },
})
expect(mockHandleTextChange).toHaveBeenCalledTimes(1)
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest'
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
import { extensionToFileType } from '../extension-to-file-type'
describe('extensionToFileType', () => {
// PDF extension
describe('pdf', () => {
it('should return pdf type when extension is pdf', () => {
expect(extensionToFileType('pdf')).toBe(FileAppearanceTypeEnum.pdf)
})
})
// Word extensions
describe('word', () => {
it('should return word type when extension is doc', () => {
expect(extensionToFileType('doc')).toBe(FileAppearanceTypeEnum.word)
})
it('should return word type when extension is docx', () => {
expect(extensionToFileType('docx')).toBe(FileAppearanceTypeEnum.word)
})
})
// Markdown extensions
describe('markdown', () => {
it('should return markdown type when extension is md', () => {
expect(extensionToFileType('md')).toBe(FileAppearanceTypeEnum.markdown)
})
it('should return markdown type when extension is mdx', () => {
expect(extensionToFileType('mdx')).toBe(FileAppearanceTypeEnum.markdown)
})
it('should return markdown type when extension is markdown', () => {
expect(extensionToFileType('markdown')).toBe(FileAppearanceTypeEnum.markdown)
})
})
// Excel / CSV extensions
describe('excel', () => {
it('should return excel type when extension is csv', () => {
expect(extensionToFileType('csv')).toBe(FileAppearanceTypeEnum.excel)
})
it('should return excel type when extension is xls', () => {
expect(extensionToFileType('xls')).toBe(FileAppearanceTypeEnum.excel)
})
it('should return excel type when extension is xlsx', () => {
expect(extensionToFileType('xlsx')).toBe(FileAppearanceTypeEnum.excel)
})
})
// Document extensions
describe('document', () => {
it('should return document type when extension is txt', () => {
expect(extensionToFileType('txt')).toBe(FileAppearanceTypeEnum.document)
})
it('should return document type when extension is epub', () => {
expect(extensionToFileType('epub')).toBe(FileAppearanceTypeEnum.document)
})
it('should return document type when extension is html', () => {
expect(extensionToFileType('html')).toBe(FileAppearanceTypeEnum.document)
})
it('should return document type when extension is htm', () => {
expect(extensionToFileType('htm')).toBe(FileAppearanceTypeEnum.document)
})
it('should return document type when extension is xml', () => {
expect(extensionToFileType('xml')).toBe(FileAppearanceTypeEnum.document)
})
})
// PPT extensions
describe('ppt', () => {
it('should return ppt type when extension is ppt', () => {
expect(extensionToFileType('ppt')).toBe(FileAppearanceTypeEnum.ppt)
})
it('should return ppt type when extension is pptx', () => {
expect(extensionToFileType('pptx')).toBe(FileAppearanceTypeEnum.ppt)
})
})
// Default / unknown extensions
describe('custom (default)', () => {
it('should return custom type when extension is empty string', () => {
expect(extensionToFileType('')).toBe(FileAppearanceTypeEnum.custom)
})
it('should return custom type when extension is unknown', () => {
expect(extensionToFileType('zip')).toBe(FileAppearanceTypeEnum.custom)
})
it('should return custom type when extension is uppercase (case-sensitive match)', () => {
expect(extensionToFileType('PDF')).toBe(FileAppearanceTypeEnum.custom)
})
it('should return custom type when extension is mixed case', () => {
expect(extensionToFileType('Docx')).toBe(FileAppearanceTypeEnum.custom)
})
it('should return custom type when extension has leading dot', () => {
expect(extensionToFileType('.pdf')).toBe(FileAppearanceTypeEnum.custom)
})
it('should return custom type when extension has whitespace', () => {
expect(extensionToFileType(' pdf ')).toBe(FileAppearanceTypeEnum.custom)
})
it('should return custom type for image-like extensions', () => {
expect(extensionToFileType('png')).toBe(FileAppearanceTypeEnum.custom)
expect(extensionToFileType('jpg')).toBe(FileAppearanceTypeEnum.custom)
})
})
})