mirror of https://github.com/langgenius/dify.git
2650 lines
88 KiB
TypeScript
2650 lines
88 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import type { DataSet, HitTesting, HitTestingChildChunk, HitTestingRecord, HitTestingResponse, Query } from '@/models/datasets'
|
|
import type { RetrievalConfig } from '@/types/app'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
|
|
import { RETRIEVE_METHOD } from '@/types/app'
|
|
|
|
// ============================================================================
|
|
// Imports (after mocks)
|
|
// ============================================================================
|
|
|
|
import ChildChunksItem from './components/child-chunks-item'
|
|
import ChunkDetailModal from './components/chunk-detail-modal'
|
|
import EmptyRecords from './components/empty-records'
|
|
import Mask from './components/mask'
|
|
import QueryInput from './components/query-input'
|
|
import Textarea from './components/query-input/textarea'
|
|
import Records from './components/records'
|
|
import ResultItem from './components/result-item'
|
|
import ResultItemExternal from './components/result-item-external'
|
|
import ResultItemFooter from './components/result-item-footer'
|
|
import ResultItemMeta from './components/result-item-meta'
|
|
import Score from './components/score'
|
|
import HitTestingPage from './index'
|
|
import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
|
|
import ModifyRetrievalModal from './modify-retrieval-modal'
|
|
import { extensionToFileType } from './utils/extension-to-file-type'
|
|
|
|
// Mock Toast
|
|
// Note: These components use real implementations for integration testing:
|
|
// - Toast, FloatRightContainer, Drawer, Pagination, Loading
|
|
// - RetrievalMethodConfig, EconomicalRetrievalMethodConfig
|
|
// - ImageUploaderInRetrievalTesting, retrieval-method-info, check-rerank-model
|
|
|
|
// Mock RetrievalSettings to allow triggering onChange
|
|
vi.mock('@/app/components/datasets/external-knowledge-base/create/RetrievalSettings', () => ({
|
|
default: ({ onChange }: { onChange: (data: { top_k?: number, score_threshold?: number, score_threshold_enabled?: boolean }) => void }) => {
|
|
return (
|
|
<div data-testid="retrieval-settings-mock">
|
|
<button data-testid="change-top-k" onClick={() => onChange({ top_k: 8 })}>Change Top K</button>
|
|
<button data-testid="change-score-threshold" onClick={() => onChange({ score_threshold: 0.9 })}>Change Score Threshold</button>
|
|
<button data-testid="change-score-enabled" onClick={() => onChange({ score_threshold_enabled: true })}>Change Score Enabled</button>
|
|
</div>
|
|
)
|
|
},
|
|
}))
|
|
|
|
// ============================================================================
|
|
// Mock Setup
|
|
// ============================================================================
|
|
|
|
// Mock next/navigation
|
|
vi.mock('next/navigation', () => ({
|
|
useRouter: () => ({
|
|
push: vi.fn(),
|
|
replace: vi.fn(),
|
|
}),
|
|
usePathname: () => '/test',
|
|
useSearchParams: () => new URLSearchParams(),
|
|
}))
|
|
|
|
// Mock use-context-selector
|
|
const mockDataset = {
|
|
id: 'dataset-1',
|
|
name: 'Test Dataset',
|
|
provider: 'vendor',
|
|
indexing_technique: 'high_quality' as const,
|
|
retrieval_model_dict: {
|
|
search_method: RETRIEVE_METHOD.semantic,
|
|
reranking_enable: false,
|
|
reranking_mode: undefined,
|
|
reranking_model: {
|
|
reranking_provider_name: '',
|
|
reranking_model_name: '',
|
|
},
|
|
weights: undefined,
|
|
top_k: 10,
|
|
score_threshold_enabled: false,
|
|
score_threshold: 0.5,
|
|
},
|
|
is_multimodal: false,
|
|
} as Partial<DataSet>
|
|
|
|
vi.mock('use-context-selector', () => ({
|
|
useContext: vi.fn(() => ({ dataset: mockDataset })),
|
|
useContextSelector: vi.fn((_, selector) => selector({ dataset: mockDataset })),
|
|
createContext: vi.fn(() => ({})),
|
|
}))
|
|
|
|
// Mock dataset detail context
|
|
vi.mock('@/context/dataset-detail', () => ({
|
|
default: {},
|
|
useDatasetDetailContext: vi.fn(() => ({ dataset: mockDataset })),
|
|
useDatasetDetailContextWithSelector: vi.fn((selector: (v: { dataset?: typeof mockDataset }) => unknown) =>
|
|
selector({ dataset: mockDataset as DataSet }),
|
|
),
|
|
}))
|
|
|
|
// Mock service hooks
|
|
const mockRecordsRefetch = vi.fn()
|
|
const mockHitTestingMutateAsync = vi.fn()
|
|
const mockExternalHitTestingMutateAsync = vi.fn()
|
|
|
|
vi.mock('@/service/knowledge/use-dataset', () => ({
|
|
useDatasetTestingRecords: vi.fn(() => ({
|
|
data: {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
})),
|
|
}))
|
|
|
|
vi.mock('@/service/knowledge/use-hit-testing', () => ({
|
|
useHitTesting: vi.fn(() => ({
|
|
mutateAsync: mockHitTestingMutateAsync,
|
|
isPending: false,
|
|
})),
|
|
useExternalKnowledgeBaseHitTesting: vi.fn(() => ({
|
|
mutateAsync: mockExternalHitTestingMutateAsync,
|
|
isPending: false,
|
|
})),
|
|
}))
|
|
|
|
// Mock breakpoints hook
|
|
vi.mock('@/hooks/use-breakpoints', () => ({
|
|
default: vi.fn(() => 'pc'),
|
|
MediaType: {
|
|
mobile: 'mobile',
|
|
pc: 'pc',
|
|
},
|
|
}))
|
|
|
|
// Mock timestamp hook
|
|
vi.mock('@/hooks/use-timestamp', () => ({
|
|
default: vi.fn(() => ({
|
|
formatTime: vi.fn((timestamp: number, _format: string) => new Date(timestamp * 1000).toISOString()),
|
|
})),
|
|
}))
|
|
|
|
// Mock use-common to avoid QueryClient issues in nested hooks
|
|
vi.mock('@/service/use-common', () => ({
|
|
useFileUploadConfig: vi.fn(() => ({
|
|
data: {
|
|
file_size_limit: 10,
|
|
batch_count_limit: 5,
|
|
image_file_size_limit: 5,
|
|
},
|
|
isLoading: false,
|
|
})),
|
|
}))
|
|
|
|
// Store ref to ImageUploader onChange for testing
|
|
let mockImageUploaderOnChange: ((files: Array<{ sourceUrl?: string, uploadedId?: string, mimeType: string, name: string, size: number, extension: string }>) => void) | null = null
|
|
|
|
// Mock ImageUploaderInRetrievalTesting to capture onChange
|
|
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: Array<{ sourceUrl?: string, uploadedId?: string, mimeType: string, name: string, size: number, extension: string }>) => void
|
|
}) => {
|
|
mockImageUploaderOnChange = onChange
|
|
return (
|
|
<div data-testid="image-uploader-mock">
|
|
{textArea}
|
|
{actionButton}
|
|
<button
|
|
data-testid="trigger-image-change"
|
|
onClick={() => onChange([
|
|
{
|
|
sourceUrl: 'http://example.com/new-image.png',
|
|
uploadedId: 'new-uploaded-id',
|
|
mimeType: 'image/png',
|
|
name: 'new-image.png',
|
|
size: 2000,
|
|
extension: 'png',
|
|
},
|
|
])}
|
|
>
|
|
Add Image
|
|
</button>
|
|
</div>
|
|
)
|
|
},
|
|
}))
|
|
|
|
// Mock docLink hook
|
|
vi.mock('@/context/i18n', () => ({
|
|
useDocLink: vi.fn(() => () => 'https://docs.example.com'),
|
|
}))
|
|
|
|
// Mock provider context for retrieval method config
|
|
vi.mock('@/context/provider-context', () => ({
|
|
useProviderContext: vi.fn(() => ({
|
|
supportRetrievalMethods: [
|
|
'semantic_search',
|
|
'full_text_search',
|
|
'hybrid_search',
|
|
],
|
|
})),
|
|
}))
|
|
|
|
// Mock model list hook - include all exports used by child components
|
|
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
|
useModelList: vi.fn(() => ({
|
|
data: [],
|
|
isLoading: false,
|
|
})),
|
|
useModelListAndDefaultModelAndCurrentProviderAndModel: vi.fn(() => ({
|
|
modelList: [],
|
|
defaultModel: undefined,
|
|
currentProvider: undefined,
|
|
currentModel: undefined,
|
|
})),
|
|
useModelListAndDefaultModel: vi.fn(() => ({
|
|
modelList: [],
|
|
defaultModel: undefined,
|
|
})),
|
|
useCurrentProviderAndModel: vi.fn(() => ({
|
|
currentProvider: undefined,
|
|
currentModel: undefined,
|
|
})),
|
|
useDefaultModel: vi.fn(() => ({
|
|
defaultModel: undefined,
|
|
})),
|
|
}))
|
|
|
|
// ============================================================================
|
|
// Test Wrapper with QueryClientProvider
|
|
// ============================================================================
|
|
|
|
const createTestQueryClient = () => new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
gcTime: 0,
|
|
},
|
|
mutations: {
|
|
retry: false,
|
|
},
|
|
},
|
|
})
|
|
|
|
const TestWrapper = ({ children }: { children: ReactNode }) => {
|
|
const queryClient = createTestQueryClient()
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
{children}
|
|
</QueryClientProvider>
|
|
)
|
|
}
|
|
|
|
const renderWithProviders = (ui: React.ReactElement) => {
|
|
return render(ui, { wrapper: TestWrapper })
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Factories
|
|
// ============================================================================
|
|
|
|
const createMockSegment = (overrides = {}) => ({
|
|
id: 'segment-1',
|
|
document: {
|
|
id: 'doc-1',
|
|
data_source_type: 'upload_file',
|
|
name: 'test-document.pdf',
|
|
doc_type: 'book' as const,
|
|
},
|
|
content: 'Test segment content',
|
|
sign_content: 'Test signed content',
|
|
position: 1,
|
|
word_count: 100,
|
|
tokens: 50,
|
|
keywords: ['test', 'keyword'],
|
|
hit_count: 5,
|
|
index_node_hash: 'hash-123',
|
|
answer: '',
|
|
...overrides,
|
|
})
|
|
|
|
const createMockHitTesting = (overrides = {}): HitTesting => ({
|
|
segment: createMockSegment() as HitTesting['segment'],
|
|
content: createMockSegment() as HitTesting['content'],
|
|
score: 0.85,
|
|
tsne_position: { x: 0.5, y: 0.5 },
|
|
child_chunks: null,
|
|
files: [],
|
|
...overrides,
|
|
})
|
|
|
|
const createMockChildChunk = (overrides = {}): HitTestingChildChunk => ({
|
|
id: 'child-chunk-1',
|
|
content: 'Child chunk content',
|
|
position: 1,
|
|
score: 0.9,
|
|
...overrides,
|
|
})
|
|
|
|
const createMockRecord = (overrides = {}): HitTestingRecord => ({
|
|
id: 'record-1',
|
|
source: 'hit_testing',
|
|
source_app_id: 'app-1',
|
|
created_by_role: 'account',
|
|
created_by: 'user-1',
|
|
created_at: 1609459200,
|
|
queries: [
|
|
{ content: 'Test query', content_type: 'text_query', file_info: null },
|
|
],
|
|
...overrides,
|
|
})
|
|
|
|
const createMockRetrievalConfig = (overrides = {}): RetrievalConfig => ({
|
|
search_method: RETRIEVE_METHOD.semantic,
|
|
reranking_enable: false,
|
|
reranking_mode: undefined,
|
|
reranking_model: {
|
|
reranking_provider_name: '',
|
|
reranking_model_name: '',
|
|
},
|
|
weights: undefined,
|
|
top_k: 10,
|
|
score_threshold_enabled: false,
|
|
score_threshold: 0.5,
|
|
...overrides,
|
|
} as RetrievalConfig)
|
|
|
|
// ============================================================================
|
|
// Utility Function Tests
|
|
// ============================================================================
|
|
|
|
describe('extensionToFileType', () => {
|
|
describe('PDF files', () => {
|
|
it('should return pdf type for pdf extension', () => {
|
|
expect(extensionToFileType('pdf')).toBe(FileAppearanceTypeEnum.pdf)
|
|
})
|
|
})
|
|
|
|
describe('Word files', () => {
|
|
it('should return word type for doc extension', () => {
|
|
expect(extensionToFileType('doc')).toBe(FileAppearanceTypeEnum.word)
|
|
})
|
|
|
|
it('should return word type for docx extension', () => {
|
|
expect(extensionToFileType('docx')).toBe(FileAppearanceTypeEnum.word)
|
|
})
|
|
})
|
|
|
|
describe('Markdown files', () => {
|
|
it('should return markdown type for md extension', () => {
|
|
expect(extensionToFileType('md')).toBe(FileAppearanceTypeEnum.markdown)
|
|
})
|
|
|
|
it('should return markdown type for mdx extension', () => {
|
|
expect(extensionToFileType('mdx')).toBe(FileAppearanceTypeEnum.markdown)
|
|
})
|
|
|
|
it('should return markdown type for markdown extension', () => {
|
|
expect(extensionToFileType('markdown')).toBe(FileAppearanceTypeEnum.markdown)
|
|
})
|
|
})
|
|
|
|
describe('Excel files', () => {
|
|
it('should return excel type for csv extension', () => {
|
|
expect(extensionToFileType('csv')).toBe(FileAppearanceTypeEnum.excel)
|
|
})
|
|
|
|
it('should return excel type for xls extension', () => {
|
|
expect(extensionToFileType('xls')).toBe(FileAppearanceTypeEnum.excel)
|
|
})
|
|
|
|
it('should return excel type for xlsx extension', () => {
|
|
expect(extensionToFileType('xlsx')).toBe(FileAppearanceTypeEnum.excel)
|
|
})
|
|
})
|
|
|
|
describe('Document files', () => {
|
|
it('should return document type for txt extension', () => {
|
|
expect(extensionToFileType('txt')).toBe(FileAppearanceTypeEnum.document)
|
|
})
|
|
|
|
it('should return document type for epub extension', () => {
|
|
expect(extensionToFileType('epub')).toBe(FileAppearanceTypeEnum.document)
|
|
})
|
|
|
|
it('should return document type for html extension', () => {
|
|
expect(extensionToFileType('html')).toBe(FileAppearanceTypeEnum.document)
|
|
})
|
|
|
|
it('should return document type for htm extension', () => {
|
|
expect(extensionToFileType('htm')).toBe(FileAppearanceTypeEnum.document)
|
|
})
|
|
|
|
it('should return document type for xml extension', () => {
|
|
expect(extensionToFileType('xml')).toBe(FileAppearanceTypeEnum.document)
|
|
})
|
|
})
|
|
|
|
describe('PowerPoint files', () => {
|
|
it('should return ppt type for ppt extension', () => {
|
|
expect(extensionToFileType('ppt')).toBe(FileAppearanceTypeEnum.ppt)
|
|
})
|
|
|
|
it('should return ppt type for pptx extension', () => {
|
|
expect(extensionToFileType('pptx')).toBe(FileAppearanceTypeEnum.ppt)
|
|
})
|
|
})
|
|
|
|
describe('Edge cases', () => {
|
|
it('should return custom type for unknown extension', () => {
|
|
expect(extensionToFileType('unknown')).toBe(FileAppearanceTypeEnum.custom)
|
|
})
|
|
|
|
it('should return custom type for empty string', () => {
|
|
expect(extensionToFileType('')).toBe(FileAppearanceTypeEnum.custom)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Score Component Tests
|
|
// ============================================================================
|
|
|
|
describe('Score', () => {
|
|
describe('Rendering', () => {
|
|
it('should render score with correct value', () => {
|
|
render(<Score value={0.85} />)
|
|
expect(screen.getByText('0.85')).toBeInTheDocument()
|
|
expect(screen.getByText('score')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render nothing when value is null', () => {
|
|
const { container } = render(<Score value={null} />)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render nothing when value is NaN', () => {
|
|
const { container } = render(<Score value={Number.NaN} />)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render nothing when value is 0', () => {
|
|
const { container } = render(<Score value={0} />)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should apply besideChunkName styles when prop is true', () => {
|
|
const { container } = render(<Score value={0.5} besideChunkName />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper).toHaveClass('border-l-0')
|
|
})
|
|
|
|
it('should apply rounded styles when besideChunkName is false', () => {
|
|
const { container } = render(<Score value={0.5} besideChunkName={false} />)
|
|
const wrapper = container.firstChild as HTMLElement
|
|
expect(wrapper).toHaveClass('rounded-md')
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should display full score correctly', () => {
|
|
render(<Score value={1} />)
|
|
expect(screen.getByText('1.00')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display very small score correctly', () => {
|
|
render(<Score value={0.01} />)
|
|
expect(screen.getByText('0.01')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Mask Component Tests
|
|
// ============================================================================
|
|
|
|
describe('Mask', () => {
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
const { container } = render(<Mask />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should have gradient background class', () => {
|
|
const { container } = render(<Mask />)
|
|
expect(container.firstChild).toHaveClass('bg-gradient-to-b')
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should apply custom className', () => {
|
|
const { container } = render(<Mask className="custom-class" />)
|
|
expect(container.firstChild).toHaveClass('custom-class')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// EmptyRecords Component Tests
|
|
// ============================================================================
|
|
|
|
describe('EmptyRecords', () => {
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<EmptyRecords />)
|
|
expect(screen.getByText(/noRecentTip/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render history icon', () => {
|
|
const { container } = render(<EmptyRecords />)
|
|
const icon = container.querySelector('svg')
|
|
expect(icon).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ResultItemMeta Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ResultItemMeta', () => {
|
|
const defaultProps = {
|
|
labelPrefix: 'Chunk',
|
|
positionId: 1,
|
|
wordCount: 100,
|
|
score: 0.85,
|
|
}
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ResultItemMeta {...defaultProps} />)
|
|
expect(screen.getByText(/100/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render score component', () => {
|
|
render(<ResultItemMeta {...defaultProps} />)
|
|
expect(screen.getByText('0.85')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render word count', () => {
|
|
render(<ResultItemMeta {...defaultProps} />)
|
|
expect(screen.getByText(/100/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should apply custom className', () => {
|
|
const { container } = render(<ResultItemMeta {...defaultProps} className="custom-class" />)
|
|
expect(container.firstChild).toHaveClass('custom-class')
|
|
})
|
|
|
|
it('should handle different position IDs', () => {
|
|
render(<ResultItemMeta {...defaultProps} positionId={42} />)
|
|
// Position ID is passed to SegmentIndexTag
|
|
expect(screen.getByText(/42/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ResultItemFooter Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ResultItemFooter', () => {
|
|
const mockShowDetailModal = vi.fn()
|
|
const defaultProps = {
|
|
docType: FileAppearanceTypeEnum.pdf,
|
|
docTitle: 'Test Document.pdf',
|
|
showDetailModal: mockShowDetailModal,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ResultItemFooter {...defaultProps} />)
|
|
expect(screen.getByText('Test Document.pdf')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render open button', () => {
|
|
render(<ResultItemFooter {...defaultProps} />)
|
|
expect(screen.getByText(/open/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call showDetailModal when open button is clicked', async () => {
|
|
render(<ResultItemFooter {...defaultProps} />)
|
|
|
|
const openButton = screen.getByText(/open/i).parentElement
|
|
if (openButton)
|
|
fireEvent.click(openButton)
|
|
|
|
expect(mockShowDetailModal).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ChildChunksItem Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ChildChunksItem', () => {
|
|
const mockChildChunk = createMockChildChunk()
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ChildChunksItem payload={mockChildChunk} isShowAll={false} />)
|
|
expect(screen.getByText(/Child chunk content/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render position identifier', () => {
|
|
render(<ChildChunksItem payload={mockChildChunk} isShowAll={false} />)
|
|
// The C- and position number are in the same element
|
|
expect(screen.getByText(/C-/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render score', () => {
|
|
render(<ChildChunksItem payload={mockChildChunk} isShowAll={false} />)
|
|
expect(screen.getByText('0.90')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Props', () => {
|
|
it('should apply line-clamp when isShowAll is false', () => {
|
|
const { container } = render(<ChildChunksItem payload={mockChildChunk} isShowAll={false} />)
|
|
expect(container.firstChild).toHaveClass('line-clamp-2')
|
|
})
|
|
|
|
it('should not apply line-clamp when isShowAll is true', () => {
|
|
const { container } = render(<ChildChunksItem payload={mockChildChunk} isShowAll={true} />)
|
|
expect(container.firstChild).not.toHaveClass('line-clamp-2')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ResultItem Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ResultItem', () => {
|
|
const mockHitTesting = createMockHitTesting()
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ResultItem payload={mockHitTesting} />)
|
|
// Document name should be visible
|
|
expect(screen.getByText('test-document.pdf')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render score', () => {
|
|
render(<ResultItem payload={mockHitTesting} />)
|
|
expect(screen.getByText('0.85')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render document name in footer', () => {
|
|
render(<ResultItem payload={mockHitTesting} />)
|
|
expect(screen.getByText('test-document.pdf')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should open detail modal when clicked', async () => {
|
|
render(<ResultItem payload={mockHitTesting} />)
|
|
|
|
const item = screen.getByText('test-document.pdf').closest('.cursor-pointer')
|
|
if (item)
|
|
fireEvent.click(item)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/chunkDetail/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Parent-Child Retrieval', () => {
|
|
it('should render child chunks when present', () => {
|
|
const payloadWithChildren = createMockHitTesting({
|
|
child_chunks: [createMockChildChunk()],
|
|
})
|
|
|
|
render(<ResultItem payload={payloadWithChildren} />)
|
|
expect(screen.getByText(/hitChunks/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should toggle fold state when child chunks header is clicked', async () => {
|
|
const payloadWithChildren = createMockHitTesting({
|
|
child_chunks: [createMockChildChunk()],
|
|
})
|
|
|
|
render(<ResultItem payload={payloadWithChildren} />)
|
|
|
|
// Child chunks should be visible by default (not folded)
|
|
expect(screen.getByText(/Child chunk content/)).toBeInTheDocument()
|
|
|
|
// Click to fold
|
|
const toggleButton = screen.getByText(/hitChunks/i).parentElement
|
|
if (toggleButton) {
|
|
fireEvent.click(toggleButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText(/Child chunk content/)).not.toBeInTheDocument()
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Keywords', () => {
|
|
it('should render keywords when present and no child chunks', () => {
|
|
const payload = createMockHitTesting({
|
|
segment: createMockSegment({ keywords: ['keyword1', 'keyword2'] }),
|
|
child_chunks: null,
|
|
})
|
|
|
|
render(<ResultItem payload={payload} />)
|
|
expect(screen.getByText('keyword1')).toBeInTheDocument()
|
|
expect(screen.getByText('keyword2')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render keywords when child chunks are present', () => {
|
|
const payload = createMockHitTesting({
|
|
segment: createMockSegment({ keywords: ['keyword1'] }),
|
|
child_chunks: [createMockChildChunk()],
|
|
})
|
|
|
|
render(<ResultItem payload={payload} />)
|
|
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ResultItemExternal Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ResultItemExternal', () => {
|
|
const defaultProps = {
|
|
payload: {
|
|
content: 'External content',
|
|
title: 'External Title',
|
|
score: 0.75,
|
|
metadata: {
|
|
'x-amz-bedrock-kb-source-uri': 'source-uri',
|
|
'x-amz-bedrock-kb-data-source-id': 'data-source-id',
|
|
},
|
|
},
|
|
positionId: 1,
|
|
}
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ResultItemExternal {...defaultProps} />)
|
|
expect(screen.getByText('External content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render title in footer', () => {
|
|
render(<ResultItemExternal {...defaultProps} />)
|
|
expect(screen.getByText('External Title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render score', () => {
|
|
render(<ResultItemExternal {...defaultProps} />)
|
|
expect(screen.getByText('0.75')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should open detail modal when clicked', async () => {
|
|
render(<ResultItemExternal {...defaultProps} />)
|
|
|
|
const item = screen.getByText('External content').closest('.cursor-pointer')
|
|
if (item)
|
|
fireEvent.click(item)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/chunkDetail/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Textarea Component Tests
|
|
// ============================================================================
|
|
|
|
describe('Textarea', () => {
|
|
const mockHandleTextChange = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<Textarea text="" handleTextChange={mockHandleTextChange} />)
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display text value', () => {
|
|
render(<Textarea text="Test input" handleTextChange={mockHandleTextChange} />)
|
|
expect(screen.getByDisplayValue('Test input')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display character count', () => {
|
|
render(<Textarea text="Hello" handleTextChange={mockHandleTextChange} />)
|
|
expect(screen.getByText('5/200')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call handleTextChange when typing', async () => {
|
|
render(<Textarea text="" handleTextChange={mockHandleTextChange} />)
|
|
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'New text' } })
|
|
|
|
expect(mockHandleTextChange).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Validation', () => {
|
|
it('should show warning style when text exceeds 200 characters', () => {
|
|
const longText = 'a'.repeat(201)
|
|
const { container } = render(<Textarea text={longText} handleTextChange={mockHandleTextChange} />)
|
|
|
|
expect(container.querySelector('.border-state-destructive-active')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show warning count when text exceeds 200 characters', () => {
|
|
const longText = 'a'.repeat(201)
|
|
render(<Textarea text={longText} handleTextChange={mockHandleTextChange} />)
|
|
|
|
expect(screen.getByText('201/200')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Records Component Tests
|
|
// ============================================================================
|
|
|
|
describe('Records', () => {
|
|
const mockOnClickRecord = vi.fn()
|
|
const mockRecords = [
|
|
createMockRecord({ id: 'record-1', created_at: 1609459200 }),
|
|
createMockRecord({ id: 'record-2', created_at: 1609545600 }),
|
|
]
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<Records records={mockRecords} onClickRecord={mockOnClickRecord} />)
|
|
expect(screen.getByText(/queryContent/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render all records', () => {
|
|
render(<Records records={mockRecords} onClickRecord={mockOnClickRecord} />)
|
|
// Each record has "Test query" as content
|
|
expect(screen.getAllByText('Test query')).toHaveLength(2)
|
|
})
|
|
|
|
it('should render table headers', () => {
|
|
render(<Records records={mockRecords} onClickRecord={mockOnClickRecord} />)
|
|
expect(screen.getByText(/queryContent/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/source/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/time/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onClickRecord when a record row is clicked', async () => {
|
|
render(<Records records={mockRecords} onClickRecord={mockOnClickRecord} />)
|
|
|
|
// Find the table body row with the query content
|
|
const queryText = screen.getAllByText('Test query')[0]
|
|
const row = queryText.closest('tr')
|
|
if (row)
|
|
fireEvent.click(row)
|
|
|
|
expect(mockOnClickRecord).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should toggle sort order when time header is clicked', async () => {
|
|
render(<Records records={mockRecords} onClickRecord={mockOnClickRecord} />)
|
|
|
|
const timeHeader = screen.getByText(/time/i)
|
|
fireEvent.click(timeHeader)
|
|
|
|
// Sort order should have toggled (default is desc, now should be asc)
|
|
// The records should be reordered
|
|
await waitFor(() => {
|
|
const rows = screen.getAllByText('Test query')
|
|
expect(rows).toHaveLength(2)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Source Display', () => {
|
|
it('should display source correctly for hit_testing', () => {
|
|
render(<Records records={mockRecords} onClickRecord={mockOnClickRecord} />)
|
|
expect(screen.getAllByText(/retrieval test/i)).toHaveLength(2)
|
|
})
|
|
|
|
it('should display source correctly for app', () => {
|
|
const appRecords = [createMockRecord({ source: 'app' })]
|
|
render(<Records records={appRecords} onClickRecord={mockOnClickRecord} />)
|
|
expect(screen.getByText('app')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ModifyExternalRetrievalModal Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ModifyExternalRetrievalModal', () => {
|
|
const mockOnClose = vi.fn()
|
|
const mockOnSave = vi.fn()
|
|
const defaultProps = {
|
|
onClose: mockOnClose,
|
|
onSave: mockOnSave,
|
|
initialTopK: 4,
|
|
initialScoreThreshold: 0.5,
|
|
initialScoreThresholdEnabled: false,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
expect(screen.getByText(/settingTitle/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render cancel and save buttons', () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
expect(screen.getByText(/cancel/i)).toBeInTheDocument()
|
|
expect(screen.getByText(/save/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onClose when cancel is clicked', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByText(/cancel/i))
|
|
|
|
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onSave with settings when save is clicked', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
expect(mockOnSave).toHaveBeenCalledWith({
|
|
top_k: 4,
|
|
score_threshold: 0.5,
|
|
score_threshold_enabled: false,
|
|
})
|
|
})
|
|
|
|
it('should call onClose when close button is clicked', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
const closeButton = screen.getByRole('button', { name: '' })
|
|
fireEvent.click(closeButton)
|
|
|
|
expect(mockOnClose).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Settings Change Handling', () => {
|
|
it('should update top_k when settings change', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
// Click the button to change top_k
|
|
fireEvent.click(screen.getByTestId('change-top-k'))
|
|
|
|
// Save to verify the change
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
top_k: 8,
|
|
}))
|
|
})
|
|
|
|
it('should update score_threshold when settings change', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
// Click the button to change score_threshold
|
|
fireEvent.click(screen.getByTestId('change-score-threshold'))
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
score_threshold: 0.9,
|
|
}))
|
|
})
|
|
|
|
it('should update score_threshold_enabled when settings change', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
// Click the button to change score_threshold_enabled
|
|
fireEvent.click(screen.getByTestId('change-score-enabled'))
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
score_threshold_enabled: true,
|
|
}))
|
|
})
|
|
|
|
it('should call onClose after save', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
// onClose should be called after onSave
|
|
expect(mockOnClose).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should render with different initial values', () => {
|
|
render(
|
|
<ModifyExternalRetrievalModal
|
|
{...defaultProps}
|
|
initialTopK={10}
|
|
initialScoreThreshold={0.8}
|
|
initialScoreThresholdEnabled={true}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
expect(mockOnSave).toHaveBeenCalledWith({
|
|
top_k: 10,
|
|
score_threshold: 0.8,
|
|
score_threshold_enabled: true,
|
|
})
|
|
})
|
|
|
|
it('should handle partial settings changes', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
// Change only top_k
|
|
fireEvent.click(screen.getByTestId('change-top-k'))
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
// Should have updated top_k while keeping other values
|
|
expect(mockOnSave).toHaveBeenCalledWith({
|
|
top_k: 8,
|
|
score_threshold: 0.5,
|
|
score_threshold_enabled: false,
|
|
})
|
|
})
|
|
|
|
it('should handle multiple settings changes', async () => {
|
|
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
|
|
|
// Change multiple settings
|
|
fireEvent.click(screen.getByTestId('change-top-k'))
|
|
fireEvent.click(screen.getByTestId('change-score-threshold'))
|
|
fireEvent.click(screen.getByTestId('change-score-enabled'))
|
|
|
|
fireEvent.click(screen.getByText(/save/i))
|
|
|
|
expect(mockOnSave).toHaveBeenCalledWith({
|
|
top_k: 8,
|
|
score_threshold: 0.9,
|
|
score_threshold_enabled: true,
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ModifyRetrievalModal Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ModifyRetrievalModal', () => {
|
|
const mockOnHide = vi.fn()
|
|
const mockOnSave = vi.fn()
|
|
const defaultProps = {
|
|
indexMethod: 'high_quality',
|
|
value: createMockRetrievalConfig(),
|
|
isShow: true,
|
|
onHide: mockOnHide,
|
|
onSave: mockOnSave,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing when isShow is true', () => {
|
|
const { container } = renderWithProviders(<ModifyRetrievalModal {...defaultProps} />)
|
|
// Modal should be rendered
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render nothing when isShow is false', () => {
|
|
const { container } = renderWithProviders(<ModifyRetrievalModal {...defaultProps} isShow={false} />)
|
|
expect(container.firstChild).toBeNull()
|
|
})
|
|
|
|
it('should render cancel and save buttons', () => {
|
|
renderWithProviders(<ModifyRetrievalModal {...defaultProps} />)
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
|
|
it('should render learn more link', () => {
|
|
renderWithProviders(<ModifyRetrievalModal {...defaultProps} />)
|
|
const link = screen.getByRole('link')
|
|
expect(link).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onHide when cancel button is clicked', async () => {
|
|
renderWithProviders(<ModifyRetrievalModal {...defaultProps} />)
|
|
|
|
// Find cancel button (second to last button typically)
|
|
const buttons = screen.getAllByRole('button')
|
|
const cancelButton = buttons.find(btn => btn.textContent?.toLowerCase().includes('cancel'))
|
|
if (cancelButton)
|
|
fireEvent.click(cancelButton)
|
|
|
|
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onHide when close icon is clicked', async () => {
|
|
const { container } = renderWithProviders(<ModifyRetrievalModal {...defaultProps} />)
|
|
|
|
// Find close button by its position (usually has the close icon)
|
|
const closeButton = container.querySelector('.cursor-pointer')
|
|
if (closeButton)
|
|
fireEvent.click(closeButton)
|
|
|
|
expect(mockOnHide).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onSave when save button is clicked', async () => {
|
|
renderWithProviders(<ModifyRetrievalModal {...defaultProps} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const saveButton = buttons.find(btn => btn.textContent?.toLowerCase().includes('save'))
|
|
if (saveButton)
|
|
fireEvent.click(saveButton)
|
|
|
|
expect(mockOnSave).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Index Method', () => {
|
|
it('should render for high_quality index method', () => {
|
|
const { container } = renderWithProviders(<ModifyRetrievalModal {...defaultProps} indexMethod="high_quality" />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render for economy index method', () => {
|
|
const { container } = renderWithProviders(<ModifyRetrievalModal {...defaultProps} indexMethod="economy" />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ChunkDetailModal Component Tests
|
|
// ============================================================================
|
|
|
|
describe('ChunkDetailModal', () => {
|
|
const mockOnHide = vi.fn()
|
|
const mockPayload = createMockHitTesting()
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
render(<ChunkDetailModal payload={mockPayload} onHide={mockOnHide} />)
|
|
expect(screen.getByText(/chunkDetail/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render document name', () => {
|
|
render(<ChunkDetailModal payload={mockPayload} onHide={mockOnHide} />)
|
|
expect(screen.getByText('test-document.pdf')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render score', () => {
|
|
render(<ChunkDetailModal payload={mockPayload} onHide={mockOnHide} />)
|
|
expect(screen.getByText('0.85')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Parent-Child Retrieval', () => {
|
|
it('should render child chunks section when present', () => {
|
|
const payloadWithChildren = createMockHitTesting({
|
|
child_chunks: [createMockChildChunk()],
|
|
})
|
|
|
|
render(<ChunkDetailModal payload={payloadWithChildren} onHide={mockOnHide} />)
|
|
expect(screen.getByText(/hitChunks/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Keywords', () => {
|
|
it('should render keywords section when present and no child chunks', () => {
|
|
const payload = createMockHitTesting({
|
|
segment: createMockSegment({ keywords: ['keyword1', 'keyword2'] }),
|
|
child_chunks: null,
|
|
})
|
|
|
|
render(<ChunkDetailModal payload={payload} onHide={mockOnHide} />)
|
|
// Keywords should be rendered as tags
|
|
expect(screen.getByText('keyword1')).toBeInTheDocument()
|
|
expect(screen.getByText('keyword2')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Q&A Mode', () => {
|
|
it('should render Q&A format when answer is present', () => {
|
|
const payload = createMockHitTesting({
|
|
segment: createMockSegment({
|
|
content: 'Question content',
|
|
answer: 'Answer content',
|
|
}),
|
|
})
|
|
|
|
render(<ChunkDetailModal payload={payload} onHide={mockOnHide} />)
|
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
|
expect(screen.getByText('A')).toBeInTheDocument()
|
|
expect(screen.getByText('Question content')).toBeInTheDocument()
|
|
expect(screen.getByText('Answer content')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// QueryInput Component Tests
|
|
// ============================================================================
|
|
|
|
describe('QueryInput', () => {
|
|
const mockSetHitResult = vi.fn()
|
|
const mockSetExternalHitResult = vi.fn()
|
|
const mockOnUpdateList = vi.fn()
|
|
const mockSetQueries = vi.fn()
|
|
const mockOnClickRetrievalMethod = vi.fn()
|
|
const mockOnSubmit = vi.fn()
|
|
|
|
const defaultProps = {
|
|
setHitResult: mockSetHitResult,
|
|
setExternalHitResult: mockSetExternalHitResult,
|
|
onUpdateList: mockOnUpdateList,
|
|
loading: false,
|
|
queries: [] as Query[],
|
|
setQueries: mockSetQueries,
|
|
isExternal: false,
|
|
onClickRetrievalMethod: mockOnClickRetrievalMethod,
|
|
retrievalConfig: createMockRetrievalConfig(),
|
|
isEconomy: false,
|
|
onSubmit: mockOnSubmit,
|
|
hitTestingMutation: mockHitTestingMutateAsync,
|
|
externalKnowledgeBaseHitTestingMutation: mockExternalHitTestingMutateAsync,
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
const { container } = render(<QueryInput {...defaultProps} />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render textarea', () => {
|
|
render(<QueryInput {...defaultProps} />)
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render testing button', () => {
|
|
render(<QueryInput {...defaultProps} />)
|
|
// Find button by role
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should update queries when text changes', async () => {
|
|
render(<QueryInput {...defaultProps} />)
|
|
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'New query' } })
|
|
|
|
expect(mockSetQueries).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should have disabled button when text is empty', () => {
|
|
render(<QueryInput {...defaultProps} />)
|
|
|
|
// Find the primary/submit button
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
expect(submitButton).toBeDisabled()
|
|
})
|
|
|
|
it('should enable button when text is present', () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
render(<QueryInput {...defaultProps} queries={queries} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
expect(submitButton).not.toBeDisabled()
|
|
})
|
|
|
|
it('should disable button when text exceeds 200 characters', () => {
|
|
const longQuery: Query[] = [{ content: 'a'.repeat(201), content_type: 'text_query', file_info: null }]
|
|
render(<QueryInput {...defaultProps} queries={longQuery} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
expect(submitButton).toBeDisabled()
|
|
})
|
|
|
|
it('should show loading state on button when loading', () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
render(<QueryInput {...defaultProps} queries={queries} loading={true} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
// Button should have disabled styling classes
|
|
expect(submitButton).toHaveClass('disabled:btn-disabled')
|
|
})
|
|
})
|
|
|
|
describe('External Mode', () => {
|
|
it('should render settings button for external mode', () => {
|
|
render(<QueryInput {...defaultProps} isExternal={true} />)
|
|
// In external mode, there should be a settings button
|
|
const buttons = screen.getAllByRole('button')
|
|
expect(buttons.length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
|
|
it('should open settings modal when settings button is clicked', async () => {
|
|
renderWithProviders(<QueryInput {...defaultProps} isExternal={true} />)
|
|
|
|
// Find the settings button (not the submit button)
|
|
const buttons = screen.getAllByRole('button')
|
|
const settingsButton = buttons.find(btn => !btn.classList.contains('w-[88px]'))
|
|
if (settingsButton)
|
|
fireEvent.click(settingsButton)
|
|
|
|
await waitFor(() => {
|
|
// The modal should render - look for more buttons after modal opens
|
|
expect(screen.getAllByRole('button').length).toBeGreaterThan(2)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Non-External Mode', () => {
|
|
it('should render retrieval method selector for non-external mode', () => {
|
|
const { container } = renderWithProviders(<QueryInput {...defaultProps} isExternal={false} />)
|
|
// Should have the retrieval method display (a clickable div)
|
|
const methodSelector = container.querySelector('.cursor-pointer')
|
|
expect(methodSelector).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call onClickRetrievalMethod when clicked', async () => {
|
|
const { container } = renderWithProviders(<QueryInput {...defaultProps} isExternal={false} />)
|
|
|
|
// Find the method selector (the cursor-pointer div that's not a button)
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(el => !el.closest('button'))
|
|
if (methodSelector)
|
|
fireEvent.click(methodSelector)
|
|
|
|
expect(mockOnClickRetrievalMethod).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('Submission', () => {
|
|
it('should call hitTestingMutation when submit is clicked for non-external', async () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
mockHitTestingMutateAsync.mockResolvedValue({ records: [] })
|
|
|
|
render(<QueryInput {...defaultProps} queries={queries} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockHitTestingMutateAsync).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call externalKnowledgeBaseHitTestingMutation when submit is clicked for external', async () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
mockExternalHitTestingMutateAsync.mockResolvedValue({ records: [] })
|
|
|
|
render(<QueryInput {...defaultProps} queries={queries} isExternal={true} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockExternalHitTestingMutateAsync).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call setHitResult and onUpdateList on successful non-external submission', async () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
const mockResponse = { query: { content: 'test' }, records: [] }
|
|
|
|
mockHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
options?.onSuccess?.(mockResponse)
|
|
return mockResponse
|
|
})
|
|
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={queries} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockSetHitResult).toHaveBeenCalledWith(mockResponse)
|
|
expect(mockOnUpdateList).toHaveBeenCalled()
|
|
expect(mockOnSubmit).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call setExternalHitResult and onUpdateList on successful external submission', async () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
const mockResponse = { query: { content: 'test' }, records: [] }
|
|
|
|
mockExternalHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
options?.onSuccess?.(mockResponse)
|
|
return mockResponse
|
|
})
|
|
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={queries} isExternal={true} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockSetExternalHitResult).toHaveBeenCalledWith(mockResponse)
|
|
expect(mockOnUpdateList).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Image Queries', () => {
|
|
it('should handle queries with image_query type', () => {
|
|
const queriesWithImages: Query[] = [
|
|
{ content: 'Test query', content_type: 'text_query', file_info: null },
|
|
{
|
|
content: 'http://example.com/image.png',
|
|
content_type: 'image_query',
|
|
file_info: {
|
|
id: 'file-1',
|
|
name: 'image.png',
|
|
size: 1000,
|
|
mime_type: 'image/png',
|
|
extension: 'png',
|
|
source_url: 'http://example.com/image.png',
|
|
},
|
|
},
|
|
]
|
|
|
|
const { container } = renderWithProviders(<QueryInput {...defaultProps} queries={queriesWithImages} />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should disable button when images are not all uploaded', () => {
|
|
const queriesWithUnuploadedImages: Query[] = [
|
|
{
|
|
content: 'http://example.com/image.png',
|
|
content_type: 'image_query',
|
|
file_info: {
|
|
id: '', // Empty id means not uploaded
|
|
name: 'image.png',
|
|
size: 1000,
|
|
mime_type: 'image/png',
|
|
extension: 'png',
|
|
source_url: 'http://example.com/image.png',
|
|
},
|
|
},
|
|
]
|
|
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={queriesWithUnuploadedImages} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
expect(submitButton).toBeDisabled()
|
|
})
|
|
|
|
it('should enable button when all images are uploaded', () => {
|
|
const queriesWithUploadedImages: Query[] = [
|
|
{ content: 'Test query', content_type: 'text_query', file_info: null },
|
|
{
|
|
content: 'http://example.com/image.png',
|
|
content_type: 'image_query',
|
|
file_info: {
|
|
id: 'uploaded-file-1',
|
|
name: 'image.png',
|
|
size: 1000,
|
|
mime_type: 'image/png',
|
|
extension: 'png',
|
|
source_url: 'http://example.com/image.png',
|
|
},
|
|
},
|
|
]
|
|
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={queriesWithUploadedImages} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
expect(submitButton).not.toBeDisabled()
|
|
})
|
|
|
|
it('should call setQueries with image queries when images are added', async () => {
|
|
renderWithProviders(<QueryInput {...defaultProps} />)
|
|
|
|
// Trigger image change via mock button
|
|
fireEvent.click(screen.getByTestId('trigger-image-change'))
|
|
|
|
expect(mockSetQueries).toHaveBeenCalledWith(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
content_type: 'image_query',
|
|
file_info: expect.objectContaining({
|
|
name: 'new-image.png',
|
|
mime_type: 'image/png',
|
|
}),
|
|
}),
|
|
]),
|
|
)
|
|
})
|
|
|
|
it('should replace existing image queries when new images are added', async () => {
|
|
const existingQueries: Query[] = [
|
|
{ content: 'text', content_type: 'text_query', file_info: null },
|
|
{
|
|
content: 'old-image',
|
|
content_type: 'image_query',
|
|
file_info: {
|
|
id: 'old-id',
|
|
name: 'old.png',
|
|
size: 500,
|
|
mime_type: 'image/png',
|
|
extension: 'png',
|
|
source_url: 'http://example.com/old.png',
|
|
},
|
|
},
|
|
]
|
|
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={existingQueries} />)
|
|
|
|
// Trigger image change - should replace existing images
|
|
fireEvent.click(screen.getByTestId('trigger-image-change'))
|
|
|
|
expect(mockSetQueries).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle empty source URL in file', async () => {
|
|
// Mock the onChange to return file without sourceUrl
|
|
renderWithProviders(<QueryInput {...defaultProps} />)
|
|
|
|
// The component should handle files with missing sourceUrl
|
|
if (mockImageUploaderOnChange) {
|
|
mockImageUploaderOnChange([
|
|
{
|
|
sourceUrl: undefined,
|
|
uploadedId: 'id-1',
|
|
mimeType: 'image/png',
|
|
name: 'image.png',
|
|
size: 1000,
|
|
extension: 'png',
|
|
},
|
|
])
|
|
}
|
|
|
|
expect(mockSetQueries).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle file without uploadedId', async () => {
|
|
renderWithProviders(<QueryInput {...defaultProps} />)
|
|
|
|
if (mockImageUploaderOnChange) {
|
|
mockImageUploaderOnChange([
|
|
{
|
|
sourceUrl: 'http://example.com/img.png',
|
|
uploadedId: undefined,
|
|
mimeType: 'image/png',
|
|
name: 'image.png',
|
|
size: 1000,
|
|
extension: 'png',
|
|
},
|
|
])
|
|
}
|
|
|
|
expect(mockSetQueries).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Economy Mode', () => {
|
|
it('should use keyword search method when isEconomy is true', async () => {
|
|
const queries: Query[] = [{ content: 'Test query', content_type: 'text_query', file_info: null }]
|
|
mockHitTestingMutateAsync.mockResolvedValue({ records: [] })
|
|
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={queries} isEconomy={true} />)
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockHitTestingMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
retrieval_model: expect.objectContaining({
|
|
search_method: 'keyword_search',
|
|
}),
|
|
}),
|
|
expect.anything(),
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Text Query Handling', () => {
|
|
it('should add new text query when none exists', async () => {
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={[]} />)
|
|
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'New query' } })
|
|
|
|
expect(mockSetQueries).toHaveBeenCalledWith([
|
|
expect.objectContaining({
|
|
content: 'New query',
|
|
content_type: 'text_query',
|
|
}),
|
|
])
|
|
})
|
|
|
|
it('should update existing text query', async () => {
|
|
const existingQueries: Query[] = [{ content: 'Old query', content_type: 'text_query', file_info: null }]
|
|
renderWithProviders(<QueryInput {...defaultProps} queries={existingQueries} />)
|
|
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Updated query' } })
|
|
|
|
expect(mockSetQueries).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('External Settings Modal', () => {
|
|
it('should save external retrieval settings when modal saves', async () => {
|
|
renderWithProviders(<QueryInput {...defaultProps} isExternal={true} />)
|
|
|
|
// Open settings modal
|
|
const buttons = screen.getAllByRole('button')
|
|
const settingsButton = buttons.find(btn => !btn.classList.contains('w-[88px]'))
|
|
if (settingsButton)
|
|
fireEvent.click(settingsButton)
|
|
|
|
await waitFor(() => {
|
|
// Modal should be open - look for save button in modal
|
|
const allButtons = screen.getAllByRole('button')
|
|
expect(allButtons.length).toBeGreaterThan(2)
|
|
})
|
|
|
|
// Click save in modal
|
|
const saveButton = screen.getByText(/save/i)
|
|
fireEvent.click(saveButton)
|
|
|
|
// Modal should close
|
|
await waitFor(() => {
|
|
const buttonsAfterClose = screen.getAllByRole('button')
|
|
// Should have fewer buttons after modal closes
|
|
expect(buttonsAfterClose.length).toBeLessThanOrEqual(screen.getAllByRole('button').length)
|
|
})
|
|
})
|
|
|
|
it('should close settings modal when close button is clicked', async () => {
|
|
renderWithProviders(<QueryInput {...defaultProps} isExternal={true} />)
|
|
|
|
// Open settings modal
|
|
const buttons = screen.getAllByRole('button')
|
|
const settingsButton = buttons.find(btn => !btn.classList.contains('w-[88px]'))
|
|
if (settingsButton)
|
|
fireEvent.click(settingsButton)
|
|
|
|
await waitFor(() => {
|
|
const allButtons = screen.getAllByRole('button')
|
|
expect(allButtons.length).toBeGreaterThan(2)
|
|
})
|
|
|
|
// Click cancel
|
|
const cancelButton = screen.getByText(/cancel/i)
|
|
fireEvent.click(cancelButton)
|
|
|
|
// Component should still be functional
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// HitTestingPage Component Tests
|
|
// ============================================================================
|
|
|
|
describe('HitTestingPage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render without crashing', () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render page title', () => {
|
|
renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
// Look for heading element
|
|
const heading = screen.getByRole('heading', { level: 1 })
|
|
expect(heading).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render records section', () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
// The records section should be present
|
|
expect(container.querySelector('.flex-col')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render query input', () => {
|
|
renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Loading States', () => {
|
|
it('should show loading when records are loading', async () => {
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: undefined,
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: true,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
// Loading component should be visible - look for the loading animation
|
|
const loadingElement = container.querySelector('[class*="animate"]') || container.querySelector('.flex-1')
|
|
expect(loadingElement).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Empty States', () => {
|
|
it('should show empty records when no data', () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
// EmptyRecords component should be rendered - check that the component is mounted
|
|
// The EmptyRecords has a specific structure with bg-workflow-process-bg class
|
|
const mainContainer = container.querySelector('.flex.h-full')
|
|
expect(mainContainer).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Records Display', () => {
|
|
it('should display records when data is present', async () => {
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: [createMockRecord()],
|
|
total: 1,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
expect(screen.getByText('Test query')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Pagination', () => {
|
|
it('should show pagination when total exceeds limit', async () => {
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: Array.from({ length: 10 }, (_, i) => createMockRecord({ id: `record-${i}` })),
|
|
total: 25,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: true,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
// Pagination should be visible - look for pagination controls
|
|
const paginationElement = container.querySelector('[class*="pagination"]') || container.querySelector('nav')
|
|
expect(paginationElement || screen.getAllByText('Test query').length > 0).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('Right Panel', () => {
|
|
it('should render right panel container', () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
// The right panel should be present (on non-mobile)
|
|
const rightPanel = container.querySelector('.rounded-tl-2xl')
|
|
expect(rightPanel).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Retrieval Modal', () => {
|
|
it('should open retrieval modal when method is clicked', async () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Find the method selector (cursor-pointer div with the retrieval method)
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(el => !el.closest('button') && !el.closest('tr'))
|
|
|
|
// Verify we found a method selector to click
|
|
expect(methodSelector).toBeTruthy()
|
|
|
|
if (methodSelector)
|
|
fireEvent.click(methodSelector)
|
|
|
|
// The component should still be functional after the click
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Hit Results Display', () => {
|
|
it('should display hit results when hitResult has records', async () => {
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// The right panel should show empty state initially
|
|
expect(container.querySelector('.rounded-tl-2xl')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render loading skeleton when retrieval is in progress', async () => {
|
|
const { useHitTesting } = await import('@/service/knowledge/use-hit-testing')
|
|
vi.mocked(useHitTesting).mockReturnValue({
|
|
mutateAsync: mockHitTestingMutateAsync,
|
|
isPending: true,
|
|
} as unknown as ReturnType<typeof useHitTesting>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Component should render without crashing
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render results when hit testing returns data', async () => {
|
|
// This test simulates the flow of getting hit results
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// The component should render the result display area
|
|
expect(container.querySelector('.bg-background-body')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Record Interaction', () => {
|
|
it('should update queries when a record is clicked', async () => {
|
|
const mockRecord = createMockRecord({
|
|
queries: [
|
|
{ content: 'Record query text', content_type: 'text_query', file_info: null },
|
|
],
|
|
})
|
|
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: [mockRecord],
|
|
total: 1,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Find and click the record row
|
|
const recordText = screen.getByText('Record query text')
|
|
const row = recordText.closest('tr')
|
|
if (row)
|
|
fireEvent.click(row)
|
|
|
|
// The query input should be updated - this causes re-render with new key
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('External Dataset', () => {
|
|
it('should render external dataset UI when provider is external', async () => {
|
|
// Mock dataset with external provider
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Component should render
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Mobile View', () => {
|
|
it('should handle mobile breakpoint', async () => {
|
|
// Mock mobile breakpoint
|
|
const useBreakpoints = await import('@/hooks/use-breakpoints')
|
|
vi.mocked(useBreakpoints.default).mockReturnValue('mobile' as unknown as ReturnType<typeof useBreakpoints.default>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Component should still render
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('useEffect for mobile panel', () => {
|
|
it('should update right panel visibility based on mobile state', async () => {
|
|
const useBreakpoints = await import('@/hooks/use-breakpoints')
|
|
|
|
// First render with desktop
|
|
vi.mocked(useBreakpoints.default).mockReturnValue('pc' as unknown as ReturnType<typeof useBreakpoints.default>)
|
|
|
|
const { rerender, container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
|
|
// Re-render with mobile
|
|
vi.mocked(useBreakpoints.default).mockReturnValue('mobile' as unknown as ReturnType<typeof useBreakpoints.default>)
|
|
|
|
rerender(
|
|
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
|
<HitTestingPage datasetId="dataset-1" />
|
|
</QueryClientProvider>,
|
|
)
|
|
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Integration Tests
|
|
// ============================================================================
|
|
|
|
describe('Integration: Hit Testing Flow', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockHitTestingMutateAsync.mockReset()
|
|
mockExternalHitTestingMutateAsync.mockReset()
|
|
})
|
|
|
|
it('should complete a full hit testing flow', async () => {
|
|
const mockResponse: HitTestingResponse = {
|
|
query: { content: 'Test query', tsne_position: { x: 0, y: 0 } },
|
|
records: [createMockHitTesting()],
|
|
}
|
|
|
|
mockHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
options?.onSuccess?.(mockResponse)
|
|
return mockResponse
|
|
})
|
|
|
|
renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Type query
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Test query' } })
|
|
|
|
// Find submit button by class
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
expect(submitButton).not.toBeDisabled()
|
|
})
|
|
|
|
it('should handle API error gracefully', async () => {
|
|
mockHitTestingMutateAsync.mockRejectedValue(new Error('API Error'))
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Type query
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Test query' } })
|
|
|
|
// Component should still be functional - check for the main container
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render hit results after successful submission', async () => {
|
|
const mockHitTestingRecord = createMockHitTesting()
|
|
const mockResponse: HitTestingResponse = {
|
|
query: { content: 'Test query', tsne_position: { x: 0, y: 0 } },
|
|
records: [mockHitTestingRecord],
|
|
}
|
|
|
|
mockHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
// Call onSuccess synchronously to ensure state is updated
|
|
if (options?.onSuccess)
|
|
options.onSuccess(mockResponse)
|
|
return mockResponse
|
|
})
|
|
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: {
|
|
data: [],
|
|
total: 0,
|
|
page: 1,
|
|
limit: 10,
|
|
has_more: false,
|
|
},
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Type query
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Test query' } })
|
|
|
|
// Submit
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
// Verify the component is still rendered after submission
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render ResultItem components for non-external results', async () => {
|
|
const mockResponse: HitTestingResponse = {
|
|
query: { content: 'Test query', tsne_position: { x: 0, y: 0 } },
|
|
records: [
|
|
createMockHitTesting({ score: 0.95 }),
|
|
createMockHitTesting({ score: 0.85 }),
|
|
],
|
|
}
|
|
|
|
mockHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
if (options?.onSuccess)
|
|
options.onSuccess(mockResponse)
|
|
return mockResponse
|
|
})
|
|
|
|
const { useDatasetTestingRecords } = await import('@/service/knowledge/use-dataset')
|
|
vi.mocked(useDatasetTestingRecords).mockReturnValue({
|
|
data: { data: [], total: 0, page: 1, limit: 10, has_more: false },
|
|
refetch: mockRecordsRefetch,
|
|
isLoading: false,
|
|
} as unknown as ReturnType<typeof useDatasetTestingRecords>)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Submit a query
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Test query' } })
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
// Verify component is rendered after submission
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render external results when dataset is external', async () => {
|
|
const mockExternalResponse = {
|
|
query: { content: 'test' },
|
|
records: [
|
|
{
|
|
title: 'External Result 1',
|
|
content: 'External content',
|
|
score: 0.9,
|
|
metadata: {},
|
|
},
|
|
],
|
|
}
|
|
|
|
mockExternalHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
if (options?.onSuccess)
|
|
options.onSuccess(mockExternalResponse)
|
|
return mockExternalResponse
|
|
})
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Component should render
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
// Type in textarea to verify component is functional
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'Test query' } })
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Drawer and Modal Interaction Tests
|
|
// ============================================================================
|
|
|
|
describe('Drawer and Modal Interactions', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should save retrieval config when ModifyRetrievalModal onSave is called', async () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Find and click the retrieval method selector to open the drawer
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(
|
|
el => !el.closest('button') && !el.closest('tr') && el.querySelector('.text-xs'),
|
|
)
|
|
|
|
if (methodSelector) {
|
|
fireEvent.click(methodSelector)
|
|
|
|
await waitFor(() => {
|
|
// The drawer should open - verify container is still there
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
}
|
|
|
|
// Component should still be functional - verify main container
|
|
expect(container.querySelector('.overflow-y-auto')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should close retrieval modal when onHide is called', async () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Open the modal first
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(
|
|
el => !el.closest('button') && !el.closest('tr') && el.querySelector('.text-xs'),
|
|
)
|
|
|
|
if (methodSelector) {
|
|
fireEvent.click(methodSelector)
|
|
}
|
|
|
|
// Component should still be functional
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// renderHitResults Coverage Tests
|
|
// ============================================================================
|
|
|
|
describe('renderHitResults Coverage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockHitTestingMutateAsync.mockReset()
|
|
})
|
|
|
|
it('should render hit results panel with records count', async () => {
|
|
const mockRecords = [
|
|
createMockHitTesting({ score: 0.95 }),
|
|
createMockHitTesting({ score: 0.85 }),
|
|
]
|
|
const mockResponse: HitTestingResponse = {
|
|
query: { content: 'test', tsne_position: { x: 0, y: 0 } },
|
|
records: mockRecords,
|
|
}
|
|
|
|
// Make mutation call onSuccess synchronously
|
|
mockHitTestingMutateAsync.mockImplementation(async (params, options) => {
|
|
// Simulate async behavior
|
|
await Promise.resolve()
|
|
if (options?.onSuccess)
|
|
options.onSuccess(mockResponse)
|
|
return mockResponse
|
|
})
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Enter query
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'test query' } })
|
|
|
|
// Submit
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
// Verify component is functional
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should iterate through records and render ResultItem for each', async () => {
|
|
const mockRecords = [
|
|
createMockHitTesting({ score: 0.9 }),
|
|
]
|
|
|
|
mockHitTestingMutateAsync.mockImplementation(async (_params, options) => {
|
|
const response = { query: { content: 'test' }, records: mockRecords }
|
|
if (options?.onSuccess)
|
|
options.onSuccess(response)
|
|
return response
|
|
})
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'test' } })
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Drawer onSave Coverage Tests
|
|
// ============================================================================
|
|
|
|
describe('ModifyRetrievalModal onSave Coverage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('should update retrieval config when onSave is triggered', async () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Open the drawer
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(
|
|
el => !el.closest('button') && !el.closest('tr') && el.querySelector('.text-xs'),
|
|
)
|
|
|
|
if (methodSelector) {
|
|
fireEvent.click(methodSelector)
|
|
|
|
// Wait for drawer to open
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
}
|
|
|
|
// Verify component renders correctly
|
|
expect(container.querySelector('.overflow-y-auto')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should close modal after saving', async () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Open the drawer
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(
|
|
el => !el.closest('button') && !el.closest('tr') && el.querySelector('.text-xs'),
|
|
)
|
|
|
|
if (methodSelector)
|
|
fireEvent.click(methodSelector)
|
|
|
|
// Component should still be rendered
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Direct Component Coverage Tests
|
|
// ============================================================================
|
|
|
|
describe('HitTestingPage Internal Functions Coverage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockHitTestingMutateAsync.mockReset()
|
|
mockExternalHitTestingMutateAsync.mockReset()
|
|
})
|
|
|
|
it('should trigger renderHitResults when mutation succeeds with records', async () => {
|
|
// Create mock hit testing records
|
|
const mockHitRecords = [
|
|
createMockHitTesting({ score: 0.95 }),
|
|
createMockHitTesting({ score: 0.85 }),
|
|
]
|
|
|
|
const mockResponse: HitTestingResponse = {
|
|
query: { content: 'test query', tsne_position: { x: 0, y: 0 } },
|
|
records: mockHitRecords,
|
|
}
|
|
|
|
// Setup mutation to call onSuccess synchronously
|
|
mockHitTestingMutateAsync.mockImplementation((_params, options) => {
|
|
// Synchronously call onSuccess
|
|
if (options?.onSuccess)
|
|
options.onSuccess(mockResponse)
|
|
return Promise.resolve(mockResponse)
|
|
})
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Enter query and submit
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'test query' } })
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
|
|
if (submitButton) {
|
|
fireEvent.click(submitButton)
|
|
}
|
|
|
|
// Wait for state updates
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
}, { timeout: 2000 })
|
|
|
|
// Verify mutation was called
|
|
expect(mockHitTestingMutateAsync).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should handle retrieval config update via ModifyRetrievalModal', async () => {
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Find and click retrieval method to open drawer
|
|
const methodSelectors = container.querySelectorAll('.cursor-pointer')
|
|
const methodSelector = Array.from(methodSelectors).find(
|
|
el => !el.closest('button') && !el.closest('tr') && el.querySelector('.text-xs'),
|
|
)
|
|
|
|
if (methodSelector) {
|
|
fireEvent.click(methodSelector)
|
|
|
|
// Wait for drawer content
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
// Try to find save button in the drawer
|
|
const saveButtons = screen.queryAllByText(/save/i)
|
|
if (saveButtons.length > 0) {
|
|
fireEvent.click(saveButtons[0])
|
|
}
|
|
}
|
|
|
|
// Component should still work
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show hit count in results panel after successful query', async () => {
|
|
const mockRecords = [createMockHitTesting()]
|
|
const mockResponse: HitTestingResponse = {
|
|
query: { content: 'test', tsne_position: { x: 0, y: 0 } },
|
|
records: mockRecords,
|
|
}
|
|
|
|
mockHitTestingMutateAsync.mockResolvedValue(mockResponse)
|
|
|
|
const { container } = renderWithProviders(<HitTestingPage datasetId="dataset-1" />)
|
|
|
|
// Submit a query
|
|
const textarea = screen.getByRole('textbox')
|
|
fireEvent.change(textarea, { target: { value: 'test' } })
|
|
|
|
const buttons = screen.getAllByRole('button')
|
|
const submitButton = buttons.find(btn => btn.classList.contains('w-[88px]'))
|
|
|
|
if (submitButton)
|
|
fireEvent.click(submitButton)
|
|
|
|
// Verify the component renders
|
|
await waitFor(() => {
|
|
expect(container.firstChild).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Memoization Tests
|
|
// ============================================================================
|
|
|
|
describe('Memoization', () => {
|
|
describe('Score component memoization', () => {
|
|
it('should be memoized', () => {
|
|
// Score is wrapped in React.memo
|
|
const { rerender } = render(<Score value={0.5} />)
|
|
|
|
// Rerender with same props should not cause re-render
|
|
rerender(<Score value={0.5} />)
|
|
|
|
expect(screen.getByText('0.50')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Mask component memoization', () => {
|
|
it('should be memoized', () => {
|
|
const { rerender, container } = render(<Mask />)
|
|
|
|
rerender(<Mask />)
|
|
|
|
// Mask should still be rendered
|
|
expect(container.querySelector('.bg-gradient-to-b')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('EmptyRecords component memoization', () => {
|
|
it('should be memoized', () => {
|
|
const { rerender } = render(<EmptyRecords />)
|
|
|
|
rerender(<EmptyRecords />)
|
|
|
|
expect(screen.getByText(/noRecentTip/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Accessibility Tests
|
|
// ============================================================================
|
|
|
|
describe('Accessibility', () => {
|
|
describe('Textarea', () => {
|
|
it('should have placeholder text', () => {
|
|
render(<Textarea text="" handleTextChange={vi.fn()} />)
|
|
expect(screen.getByPlaceholderText(/placeholder/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Buttons', () => {
|
|
it('should have accessible buttons in QueryInput', () => {
|
|
render(
|
|
<QueryInput
|
|
setHitResult={vi.fn()}
|
|
setExternalHitResult={vi.fn()}
|
|
onUpdateList={vi.fn()}
|
|
loading={false}
|
|
queries={[]}
|
|
setQueries={vi.fn()}
|
|
isExternal={false}
|
|
onClickRetrievalMethod={vi.fn()}
|
|
retrievalConfig={createMockRetrievalConfig()}
|
|
isEconomy={false}
|
|
hitTestingMutation={vi.fn()}
|
|
externalKnowledgeBaseHitTestingMutation={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('Tables', () => {
|
|
it('should render table with proper structure', () => {
|
|
render(
|
|
<Records
|
|
records={[createMockRecord()]}
|
|
onClickRecord={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(screen.getByRole('table')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Edge Cases
|
|
// ============================================================================
|
|
|
|
describe('Edge Cases', () => {
|
|
describe('Score with edge values', () => {
|
|
it('should handle very small scores', () => {
|
|
render(<Score value={0.001} />)
|
|
expect(screen.getByText('0.00')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle scores close to 1', () => {
|
|
render(<Score value={0.999} />)
|
|
expect(screen.getByText('1.00')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Records with various sources', () => {
|
|
it('should handle plugin source', () => {
|
|
const record = createMockRecord({ source: 'plugin' })
|
|
render(<Records records={[record]} onClickRecord={vi.fn()} />)
|
|
expect(screen.getByText('plugin')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle app source', () => {
|
|
const record = createMockRecord({ source: 'app' })
|
|
render(<Records records={[record]} onClickRecord={vi.fn()} />)
|
|
expect(screen.getByText('app')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('ResultItem with various data', () => {
|
|
it('should handle empty keywords', () => {
|
|
const payload = createMockHitTesting({
|
|
segment: createMockSegment({ keywords: [] }),
|
|
child_chunks: null,
|
|
})
|
|
|
|
render(<ResultItem payload={payload} />)
|
|
// Should not render keywords section
|
|
expect(screen.queryByText('keyword')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle missing sign_content', () => {
|
|
const payload = createMockHitTesting({
|
|
segment: createMockSegment({ sign_content: '', content: 'Fallback content' }),
|
|
})
|
|
|
|
render(<ResultItem payload={payload} />)
|
|
// The document name should still be visible
|
|
expect(screen.getByText('test-document.pdf')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Records with images', () => {
|
|
it('should handle records with image queries', () => {
|
|
const recordWithImages = createMockRecord({
|
|
queries: [
|
|
{ content: 'Text query', content_type: 'text_query', file_info: null },
|
|
{
|
|
content: 'image-url',
|
|
content_type: 'image_query',
|
|
file_info: {
|
|
id: 'file-1',
|
|
name: 'image.png',
|
|
size: 1000,
|
|
mime_type: 'image/png',
|
|
extension: 'png',
|
|
source_url: 'http://example.com/image.png',
|
|
},
|
|
},
|
|
],
|
|
})
|
|
|
|
render(<Records records={[recordWithImages]} onClickRecord={vi.fn()} />)
|
|
expect(screen.getByText('Text query')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('ChunkDetailModal with files', () => {
|
|
it('should handle payload with image files', () => {
|
|
const payload = createMockHitTesting({
|
|
files: [
|
|
{
|
|
id: 'file-1',
|
|
name: 'image.png',
|
|
size: 1000,
|
|
mime_type: 'image/png',
|
|
extension: 'png',
|
|
source_url: 'http://example.com/image.png',
|
|
},
|
|
],
|
|
})
|
|
|
|
render(<ChunkDetailModal payload={payload} onHide={vi.fn()} />)
|
|
expect(screen.getByText(/chunkDetail/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|