mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox
# Conflicts: # api/uv.lock # web/app/components/apps/__tests__/app-card.spec.tsx # web/app/components/apps/__tests__/list.spec.tsx # web/app/components/datasets/create/__tests__/index.spec.tsx # web/app/components/datasets/metadata/metadata-dataset/__tests__/dataset-metadata-drawer.spec.tsx # web/app/components/plugins/readme-panel/__tests__/index.spec.tsx # web/app/components/rag-pipeline/__tests__/index.spec.tsx # web/app/components/rag-pipeline/hooks/__tests__/index.spec.ts # web/eslint-suppressions.json
This commit is contained in:
1067
web/app/components/datasets/hit-testing/__tests__/index.spec.tsx
Normal file
1067
web/app/components/datasets/hit-testing/__tests__/index.spec.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user