mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
1204 lines
42 KiB
TypeScript
1204 lines
42 KiB
TypeScript
import React from 'react'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import SegmentCard from './index'
|
|
import { type Attachment, type ChildChunkDetail, ChunkingMode, type ParentMode, type SegmentDetailModel } from '@/models/datasets'
|
|
import type { DocumentContextValue } from '@/app/components/datasets/documents/detail/context'
|
|
import type { SegmentListContextValue } from '@/app/components/datasets/documents/detail/completed'
|
|
|
|
// Mock react-i18next - external dependency
|
|
jest.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string, options?: { count?: number }) => {
|
|
if (key === 'datasetDocuments.segment.characters')
|
|
return options?.count === 1 ? 'character' : 'characters'
|
|
if (key === 'datasetDocuments.segment.childChunks')
|
|
return options?.count === 1 ? 'child chunk' : 'child chunks'
|
|
return key
|
|
},
|
|
}),
|
|
}))
|
|
|
|
// ============================================================================
|
|
// Context Mocks - need to control test scenarios
|
|
// ============================================================================
|
|
|
|
const mockDocForm = { current: ChunkingMode.text }
|
|
const mockParentMode = { current: 'paragraph' as ParentMode }
|
|
|
|
jest.mock('../../context', () => ({
|
|
useDocumentContext: (selector: (value: DocumentContextValue) => unknown) => {
|
|
const value: DocumentContextValue = {
|
|
datasetId: 'test-dataset-id',
|
|
documentId: 'test-document-id',
|
|
docForm: mockDocForm.current,
|
|
parentMode: mockParentMode.current,
|
|
}
|
|
return selector(value)
|
|
},
|
|
}))
|
|
|
|
const mockIsCollapsed = { current: true }
|
|
jest.mock('../index', () => ({
|
|
useSegmentListContext: (selector: (value: SegmentListContextValue) => unknown) => {
|
|
const value: SegmentListContextValue = {
|
|
isCollapsed: mockIsCollapsed.current,
|
|
fullScreen: false,
|
|
toggleFullScreen: jest.fn(),
|
|
currSegment: { showModal: false },
|
|
currChildChunk: { showModal: false },
|
|
}
|
|
return selector(value)
|
|
},
|
|
}))
|
|
|
|
// ============================================================================
|
|
// Component Mocks - components with complex dependencies
|
|
// ============================================================================
|
|
|
|
// StatusItem uses React Query hooks which require QueryClientProvider
|
|
jest.mock('../../../status-item', () => ({
|
|
__esModule: true,
|
|
default: ({ status, reverse, textCls }: { status: string; reverse?: boolean; textCls?: string }) => (
|
|
<div data-testid="status-item" data-status={status} data-reverse={reverse} className={textCls}>
|
|
Status: {status}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// ImageList has deep dependency: FileThumb → file-uploader → react-pdf-highlighter (ESM)
|
|
jest.mock('@/app/components/datasets/common/image-list', () => ({
|
|
__esModule: true,
|
|
default: ({ images, size, className }: { images: Array<{ sourceUrl: string; name: string }>; size?: string; className?: string }) => (
|
|
<div data-testid="image-list" data-image-count={images.length} data-size={size} className={className}>
|
|
{images.map((img, idx: number) => (
|
|
<img key={idx} src={img.sourceUrl} alt={img.name} />
|
|
))}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// Markdown uses next/dynamic and react-syntax-highlighter (ESM)
|
|
jest.mock('@/app/components/base/markdown', () => ({
|
|
__esModule: true,
|
|
Markdown: ({ content, className }: { content: string; className?: string }) => (
|
|
<div data-testid="markdown" className={`markdown-body ${className || ''}`}>{content}</div>
|
|
),
|
|
}))
|
|
|
|
// ============================================================================
|
|
// Test Data Factories
|
|
// ============================================================================
|
|
|
|
const createMockAttachment = (overrides: Partial<Attachment> = {}): Attachment => ({
|
|
id: 'attachment-1',
|
|
name: 'test-image.png',
|
|
size: 1024,
|
|
extension: 'png',
|
|
mime_type: 'image/png',
|
|
source_url: 'https://example.com/test-image.png',
|
|
...overrides,
|
|
})
|
|
|
|
const createMockChildChunk = (overrides: Partial<ChildChunkDetail> = {}): ChildChunkDetail => ({
|
|
id: 'child-chunk-1',
|
|
position: 1,
|
|
segment_id: 'segment-1',
|
|
content: 'Child chunk content',
|
|
word_count: 100,
|
|
created_at: 1700000000,
|
|
updated_at: 1700000000,
|
|
type: 'automatic',
|
|
...overrides,
|
|
})
|
|
|
|
const createMockSegmentDetail = (overrides: Partial<SegmentDetailModel & { document?: { name: string } }> = {}): SegmentDetailModel & { document?: { name: string } } => ({
|
|
id: 'segment-1',
|
|
position: 1,
|
|
document_id: 'doc-1',
|
|
content: 'Test segment content',
|
|
sign_content: 'Test signed content',
|
|
word_count: 100,
|
|
tokens: 50,
|
|
keywords: ['keyword1', 'keyword2'],
|
|
index_node_id: 'index-1',
|
|
index_node_hash: 'hash-1',
|
|
hit_count: 10,
|
|
enabled: true,
|
|
disabled_at: 0,
|
|
disabled_by: '',
|
|
status: 'completed',
|
|
created_by: 'user-1',
|
|
created_at: 1700000000,
|
|
indexing_at: 1700000100,
|
|
completed_at: 1700000200,
|
|
error: null,
|
|
stopped_at: 0,
|
|
updated_at: 1700000000,
|
|
attachments: [],
|
|
child_chunks: [],
|
|
document: { name: 'Test Document' },
|
|
...overrides,
|
|
})
|
|
|
|
const defaultFocused = { segmentIndex: false, segmentContent: false }
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('SegmentCard', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
mockDocForm.current = ChunkingMode.text
|
|
mockParentMode.current = 'paragraph'
|
|
mockIsCollapsed.current = true
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Rendering Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Rendering', () => {
|
|
it('should render loading skeleton when loading is true', () => {
|
|
render(<SegmentCard loading={true} focused={defaultFocused} />)
|
|
|
|
// ParentChunkCardSkeleton should render
|
|
expect(screen.getByTestId('parent-chunk-card-skeleton')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render segment card content when loading is false', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// ChunkContent shows sign_content first, then content
|
|
expect(screen.getByText('Test signed content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render segment index tag with correct position', () => {
|
|
const detail = createMockSegmentDetail({ position: 5 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/Chunk-05/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render word count text', () => {
|
|
const detail = createMockSegmentDetail({ word_count: 250 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('250 characters')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render hit count text', () => {
|
|
const detail = createMockSegmentDetail({ hit_count: 42 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('42 datasetDocuments.segment.hitCount')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply custom className', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard loading={false} detail={detail} className="custom-class" focused={defaultFocused} />,
|
|
)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
expect(card).toHaveClass('custom-class')
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Props Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Props', () => {
|
|
it('should use default empty object when detail is undefined', () => {
|
|
render(<SegmentCard loading={false} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/Chunk/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle archived prop correctly - switch should be disabled', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
archived={true}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const switchElement = screen.getByRole('switch')
|
|
expect(switchElement).toHaveClass('!cursor-not-allowed')
|
|
})
|
|
|
|
it('should show action buttons when embeddingAvailable is true', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.getByTestId('segment-edit-button')).toBeInTheDocument()
|
|
expect(screen.getByTestId('segment-delete-button')).toBeInTheDocument()
|
|
expect(screen.getByRole('switch')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show action buttons when embeddingAvailable is false', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={false}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply focused styles when segmentContent is focused', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
focused={{ segmentIndex: false, segmentContent: true }}
|
|
/>,
|
|
)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
expect(card).toHaveClass('bg-dataset-chunk-detail-card-hover-bg')
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// State Management Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('State Management', () => {
|
|
it('should toggle delete confirmation modal when delete button clicked', async () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const deleteButton = screen.getByTestId('segment-delete-button')
|
|
fireEvent.click(deleteButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('datasetDocuments.segment.delete')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should close delete confirmation modal when cancel is clicked', async () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const deleteButton = screen.getByTestId('segment-delete-button')
|
|
fireEvent.click(deleteButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('datasetDocuments.segment.delete')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByText('common.operation.cancel'))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('datasetDocuments.segment.delete')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Callback Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Callbacks', () => {
|
|
it('should call onClick when card is clicked in general mode', () => {
|
|
const onClick = jest.fn()
|
|
const detail = createMockSegmentDetail()
|
|
mockDocForm.current = ChunkingMode.text
|
|
|
|
render(
|
|
<SegmentCard loading={false} detail={detail} onClick={onClick} focused={defaultFocused} />,
|
|
)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
fireEvent.click(card)
|
|
|
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should not call onClick when card is clicked in full-doc mode', () => {
|
|
const onClick = jest.fn()
|
|
const detail = createMockSegmentDetail()
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
|
|
render(
|
|
<SegmentCard loading={false} detail={detail} onClick={onClick} focused={defaultFocused} />,
|
|
)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
fireEvent.click(card)
|
|
|
|
expect(onClick).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call onClick when view more button is clicked in full-doc mode', () => {
|
|
const onClick = jest.fn()
|
|
const detail = createMockSegmentDetail()
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
|
|
render(<SegmentCard loading={false} detail={detail} onClick={onClick} focused={defaultFocused} />)
|
|
|
|
const viewMoreButton = screen.getByRole('button', { name: /viewMore/i })
|
|
fireEvent.click(viewMoreButton)
|
|
|
|
expect(onClick).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onClickEdit when edit button is clicked', () => {
|
|
const onClickEdit = jest.fn()
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onClickEdit={onClickEdit}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const editButton = screen.getByTestId('segment-edit-button')
|
|
fireEvent.click(editButton)
|
|
|
|
expect(onClickEdit).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should call onDelete when confirm delete is clicked', async () => {
|
|
const onDelete = jest.fn().mockResolvedValue(undefined)
|
|
const detail = createMockSegmentDetail({ id: 'test-segment-id' })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onDelete={onDelete}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const deleteButton = screen.getByTestId('segment-delete-button')
|
|
fireEvent.click(deleteButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('datasetDocuments.segment.delete')).toBeInTheDocument()
|
|
})
|
|
|
|
fireEvent.click(screen.getByText('common.operation.sure'))
|
|
|
|
await waitFor(() => {
|
|
expect(onDelete).toHaveBeenCalledWith('test-segment-id')
|
|
})
|
|
})
|
|
|
|
it('should call onChangeSwitch when switch is toggled', async () => {
|
|
const onChangeSwitch = jest.fn().mockResolvedValue(undefined)
|
|
const detail = createMockSegmentDetail({ id: 'test-segment-id', enabled: true, status: 'completed' })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onChangeSwitch={onChangeSwitch}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const switchElement = screen.getByRole('switch')
|
|
fireEvent.click(switchElement)
|
|
|
|
await waitFor(() => {
|
|
expect(onChangeSwitch).toHaveBeenCalledWith(false, 'test-segment-id')
|
|
})
|
|
})
|
|
|
|
it('should stop propagation when edit button is clicked', () => {
|
|
const onClick = jest.fn()
|
|
const onClickEdit = jest.fn()
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onClick={onClick}
|
|
onClickEdit={onClickEdit}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const editButton = screen.getByTestId('segment-edit-button')
|
|
fireEvent.click(editButton)
|
|
|
|
expect(onClickEdit).toHaveBeenCalledTimes(1)
|
|
expect(onClick).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should stop propagation when switch area is clicked', () => {
|
|
const onClick = jest.fn()
|
|
const detail = createMockSegmentDetail({ status: 'completed' })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onClick={onClick}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const switchElement = screen.getByRole('switch')
|
|
const switchContainer = switchElement.parentElement
|
|
fireEvent.click(switchContainer!)
|
|
|
|
expect(onClick).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Memoization Logic Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Memoization Logic', () => {
|
|
it('should compute isGeneralMode correctly for text mode - show keywords', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail({ keywords: ['testkeyword'] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('testkeyword')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should compute isGeneralMode correctly for non-text mode - hide keywords', () => {
|
|
mockDocForm.current = ChunkingMode.qa
|
|
const detail = createMockSegmentDetail({ keywords: ['testkeyword'] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByText('testkeyword')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should compute isParentChildMode correctly - show parent chunk prefix', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/datasetDocuments\.segment\.parentChunk/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should compute isFullDocMode correctly - show view more button', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('common.operation.viewMore')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should compute isParagraphMode correctly and show child chunks', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'paragraph'
|
|
const childChunks = [createMockChildChunk()]
|
|
const detail = createMockSegmentDetail({ child_chunks: childChunks })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// ChildSegmentList should render
|
|
expect(screen.getByText(/child chunk/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should compute chunkEdited correctly when updated_at > created_at', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail({
|
|
created_at: 1700000000,
|
|
updated_at: 1700000001,
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('datasetDocuments.segment.edited')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show edited badge when timestamps are equal', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail({
|
|
created_at: 1700000000,
|
|
updated_at: 1700000000,
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByText('datasetDocuments.segment.edited')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show edited badge in full-doc mode', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
const detail = createMockSegmentDetail({
|
|
created_at: 1700000000,
|
|
updated_at: 1700000001,
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByText('datasetDocuments.segment.edited')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should compute contentOpacity correctly when enabled', () => {
|
|
const detail = createMockSegmentDetail({ enabled: true })
|
|
|
|
const { container } = render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const wordCount = container.querySelector('.system-xs-medium.text-text-tertiary')
|
|
expect(wordCount).not.toHaveClass('opacity-50')
|
|
})
|
|
|
|
it('should compute contentOpacity correctly when disabled', () => {
|
|
const detail = createMockSegmentDetail({ enabled: false })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// ChunkContent receives opacity class when disabled
|
|
const markdown = screen.getByTestId('markdown')
|
|
expect(markdown).toHaveClass('opacity-50')
|
|
})
|
|
|
|
it('should not apply opacity when disabled but focused', () => {
|
|
const detail = createMockSegmentDetail({ enabled: false })
|
|
|
|
const { container } = render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
focused={{ segmentIndex: false, segmentContent: true }}
|
|
/>,
|
|
)
|
|
|
|
const wordCount = container.querySelector('.system-xs-medium.text-text-tertiary')
|
|
expect(wordCount).not.toHaveClass('opacity-50')
|
|
})
|
|
|
|
it('should compute wordCountText with correct format for singular', () => {
|
|
const detail = createMockSegmentDetail({ word_count: 1 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('1 character')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Mode-specific Rendering Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Mode-specific Rendering', () => {
|
|
it('should render without padding classes in full-doc mode', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
expect(card).not.toHaveClass('pb-2')
|
|
expect(card).not.toHaveClass('pt-2.5')
|
|
})
|
|
|
|
it('should render with hover classes in non full-doc mode', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
expect(card).toHaveClass('pb-2')
|
|
expect(card).toHaveClass('pt-2.5')
|
|
})
|
|
|
|
it('should not render status item in full-doc mode', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// In full-doc mode, status item should not render
|
|
expect(screen.queryByText('Status:')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Child Segment List Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Child Segment List', () => {
|
|
it('should render ChildSegmentList when in paragraph mode with child chunks', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'paragraph'
|
|
const childChunks = [createMockChildChunk(), createMockChildChunk({ id: 'child-2', position: 2 })]
|
|
const detail = createMockSegmentDetail({ child_chunks: childChunks })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/2 child chunks/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render ChildSegmentList when child_chunks is empty', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'paragraph'
|
|
const detail = createMockSegmentDetail({ child_chunks: [] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByText(/child chunk/i)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render ChildSegmentList in full-doc mode', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
const childChunks = [createMockChildChunk()]
|
|
const detail = createMockSegmentDetail({ child_chunks: childChunks })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// In full-doc mode, ChildSegmentList should not render
|
|
expect(screen.queryByText(/1 child chunk$/i)).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should call handleAddNewChildChunk when add button is clicked', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'paragraph'
|
|
const handleAddNewChildChunk = jest.fn()
|
|
const childChunks = [createMockChildChunk()]
|
|
const detail = createMockSegmentDetail({ id: 'parent-id', child_chunks: childChunks })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
handleAddNewChildChunk={handleAddNewChildChunk}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const addButton = screen.getByText('common.operation.add')
|
|
fireEvent.click(addButton)
|
|
|
|
expect(handleAddNewChildChunk).toHaveBeenCalledWith('parent-id')
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Keywords Display Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Keywords Display', () => {
|
|
it('should render keywords with # prefix in general mode', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail({ keywords: ['keyword1', 'keyword2'] })
|
|
|
|
const { container } = render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('keyword1')).toBeInTheDocument()
|
|
expect(screen.getByText('keyword2')).toBeInTheDocument()
|
|
// Tag component shows # prefix
|
|
const hashtags = container.querySelectorAll('.text-text-quaternary')
|
|
expect(hashtags.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should not render keywords in QA mode', () => {
|
|
mockDocForm.current = ChunkingMode.qa
|
|
const detail = createMockSegmentDetail({ keywords: ['keyword1'] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render keywords in parent-child mode', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
const detail = createMockSegmentDetail({ keywords: ['keyword1'] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Images Display Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Images Display', () => {
|
|
it('should render ImageList when attachments exist', () => {
|
|
const attachments = [createMockAttachment()]
|
|
const detail = createMockSegmentDetail({ attachments })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// ImageList uses FileThumb which renders images
|
|
expect(screen.getByAltText('test-image.png')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render ImageList when attachments is empty', () => {
|
|
const detail = createMockSegmentDetail({ attachments: [] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.queryByAltText('test-image.png')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Edge Cases and Error Handling Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Edge Cases and Error Handling', () => {
|
|
it('should handle undefined detail gracefully', () => {
|
|
render(<SegmentCard loading={false} detail={undefined} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/Chunk/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty detail object gracefully', () => {
|
|
render(<SegmentCard loading={false} detail={{} as SegmentDetailModel} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/Chunk/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle missing callback functions gracefully', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onClick={undefined}
|
|
onChangeSwitch={undefined}
|
|
onDelete={undefined}
|
|
onClickEdit={undefined}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const card = screen.getByTestId('segment-card')
|
|
expect(() => fireEvent.click(card)).not.toThrow()
|
|
})
|
|
|
|
it('should handle switch being disabled when status is not completed', () => {
|
|
const detail = createMockSegmentDetail({ status: 'indexing' })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
// The Switch component uses CSS classes for disabled state, not the native disabled attribute
|
|
const switchElement = screen.getByRole('switch')
|
|
expect(switchElement).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
|
})
|
|
|
|
it('should handle zero word count', () => {
|
|
const detail = createMockSegmentDetail({ word_count: 0 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('0 characters')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle zero hit count', () => {
|
|
const detail = createMockSegmentDetail({ hit_count: 0 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('0 datasetDocuments.segment.hitCount')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle very long content', () => {
|
|
const longContent = 'A'.repeat(10000)
|
|
// ChunkContent shows sign_content first, so set it to the long content
|
|
const detail = createMockSegmentDetail({ sign_content: longContent })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(longContent)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Component Integration Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('Component Integration', () => {
|
|
it('should render real Tag component with hashtag styling', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail({ keywords: ['testkeyword'] })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('testkeyword')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render real Divider component', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const dividers = document.querySelectorAll('.bg-divider-regular')
|
|
expect(dividers.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('should render real Badge component when edited', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
const detail = createMockSegmentDetail({
|
|
created_at: 1700000000,
|
|
updated_at: 1700000001,
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const editedBadge = screen.getByText('datasetDocuments.segment.edited')
|
|
expect(editedBadge).toHaveClass('system-2xs-medium-uppercase')
|
|
})
|
|
|
|
it('should render real Switch component with correct enabled state', () => {
|
|
const detail = createMockSegmentDetail({ enabled: true, status: 'completed' })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const switchElement = screen.getByRole('switch')
|
|
expect(switchElement).toHaveClass('bg-components-toggle-bg')
|
|
})
|
|
|
|
it('should render real Switch component with unchecked state', () => {
|
|
const detail = createMockSegmentDetail({ enabled: false, status: 'completed' })
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
embeddingAvailable={true}
|
|
focused={defaultFocused}
|
|
/>,
|
|
)
|
|
|
|
const switchElement = screen.getByRole('switch')
|
|
expect(switchElement).toHaveClass('bg-components-toggle-bg-unchecked')
|
|
})
|
|
|
|
it('should render real SegmentIndexTag with position formatting', () => {
|
|
const detail = createMockSegmentDetail({ position: 1 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/Chunk-01/i)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render real SegmentIndexTag with double digit position', () => {
|
|
const detail = createMockSegmentDetail({ position: 12 })
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText(/Chunk-12/i)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// All Props Variations Tests
|
|
// --------------------------------------------------------------------------
|
|
describe('All Props Variations', () => {
|
|
it('should render correctly with all props provided', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'paragraph'
|
|
const childChunks = [createMockChildChunk()]
|
|
const attachments = [createMockAttachment()]
|
|
const detail = createMockSegmentDetail({
|
|
id: 'full-props-segment',
|
|
position: 10,
|
|
sign_content: 'Full signed content',
|
|
content: 'Full content',
|
|
word_count: 500,
|
|
hit_count: 25,
|
|
enabled: true,
|
|
keywords: ['key1', 'key2'],
|
|
child_chunks: childChunks,
|
|
attachments,
|
|
created_at: 1700000000,
|
|
updated_at: 1700000001,
|
|
status: 'completed',
|
|
})
|
|
|
|
render(
|
|
<SegmentCard
|
|
loading={false}
|
|
detail={detail}
|
|
onClick={jest.fn()}
|
|
onChangeSwitch={jest.fn()}
|
|
onDelete={jest.fn()}
|
|
onDeleteChildChunk={jest.fn()}
|
|
handleAddNewChildChunk={jest.fn()}
|
|
onClickSlice={jest.fn()}
|
|
onClickEdit={jest.fn()}
|
|
className="full-props-class"
|
|
archived={false}
|
|
embeddingAvailable={true}
|
|
focused={{ segmentIndex: true, segmentContent: true }}
|
|
/>,
|
|
)
|
|
|
|
// ChunkContent shows sign_content first
|
|
expect(screen.getByText('Full signed content')).toBeInTheDocument()
|
|
expect(screen.getByRole('switch')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render correctly with minimal props', () => {
|
|
render(<SegmentCard loading={true} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('common.operation.viewMore')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle loading transition correctly', () => {
|
|
const detail = createMockSegmentDetail()
|
|
|
|
const { rerender } = render(<SegmentCard loading={true} detail={detail} focused={defaultFocused} />)
|
|
|
|
// When loading, content should not be visible
|
|
expect(screen.queryByText('Test signed content')).not.toBeInTheDocument()
|
|
|
|
rerender(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// ChunkContent shows sign_content first
|
|
expect(screen.getByText('Test signed content')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// ChunkContent QA Mode Tests - cover lines 25-49
|
|
// --------------------------------------------------------------------------
|
|
describe('ChunkContent QA Mode', () => {
|
|
it('should render Q and A sections when answer is provided', () => {
|
|
const detail = createMockSegmentDetail({
|
|
content: 'This is the question content',
|
|
answer: 'This is the answer content',
|
|
sign_content: '',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// Should render Q label
|
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
|
// Should render A label
|
|
expect(screen.getByText('A')).toBeInTheDocument()
|
|
// Should render question content
|
|
expect(screen.getByText('This is the question content')).toBeInTheDocument()
|
|
// Should render answer content
|
|
expect(screen.getByText('This is the answer content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should apply line-clamp-2 class when isCollapsed is true in QA mode', () => {
|
|
mockIsCollapsed.current = true
|
|
const detail = createMockSegmentDetail({
|
|
content: 'Question content',
|
|
answer: 'Answer content',
|
|
sign_content: '',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// Markdown components should have line-clamp-2 class when collapsed
|
|
const markdowns = screen.getAllByTestId('markdown')
|
|
markdowns.forEach((markdown) => {
|
|
expect(markdown).toHaveClass('line-clamp-2')
|
|
})
|
|
})
|
|
|
|
it('should apply line-clamp-20 class when isCollapsed is false in QA mode', () => {
|
|
mockIsCollapsed.current = false
|
|
const detail = createMockSegmentDetail({
|
|
content: 'Question content',
|
|
answer: 'Answer content',
|
|
sign_content: '',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// Markdown components should have line-clamp-20 class when not collapsed
|
|
const markdowns = screen.getAllByTestId('markdown')
|
|
markdowns.forEach((markdown) => {
|
|
expect(markdown).toHaveClass('line-clamp-20')
|
|
})
|
|
})
|
|
|
|
it('should render QA mode with className applied to wrapper', () => {
|
|
const detail = createMockSegmentDetail({
|
|
content: 'Question',
|
|
answer: 'Answer',
|
|
sign_content: '',
|
|
enabled: false,
|
|
})
|
|
|
|
const { container } = render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// The ChunkContent wrapper should have opacity class when disabled
|
|
const qaWrapper = container.querySelector('.flex.gap-x-1')
|
|
expect(qaWrapper).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render QA mode when answer is empty string', () => {
|
|
const detail = createMockSegmentDetail({
|
|
content: 'Regular content',
|
|
answer: '',
|
|
sign_content: 'Signed content',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// Should not render Q and A labels
|
|
expect(screen.queryByText('Q')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('A')).not.toBeInTheDocument()
|
|
// Should render signed content instead
|
|
expect(screen.getByText('Signed content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render QA mode when answer is undefined', () => {
|
|
const detail = createMockSegmentDetail({
|
|
content: 'Regular content',
|
|
answer: undefined,
|
|
sign_content: 'Signed content',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
// Should not render Q and A labels
|
|
expect(screen.queryByText('Q')).not.toBeInTheDocument()
|
|
expect(screen.queryByText('A')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------------------------------------------------
|
|
// ChunkContent Non-QA Mode Tests - ensure full coverage
|
|
// --------------------------------------------------------------------------
|
|
describe('ChunkContent Non-QA Mode', () => {
|
|
it('should apply line-clamp-3 in fullDocMode', () => {
|
|
mockDocForm.current = ChunkingMode.parentChild
|
|
mockParentMode.current = 'full-doc'
|
|
const detail = createMockSegmentDetail({
|
|
sign_content: 'Content in full doc mode',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const markdown = screen.getByTestId('markdown')
|
|
expect(markdown).toHaveClass('line-clamp-3')
|
|
})
|
|
|
|
it('should apply line-clamp-2 when not fullDocMode and isCollapsed is true', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
mockIsCollapsed.current = true
|
|
const detail = createMockSegmentDetail({
|
|
sign_content: 'Collapsed content',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const markdown = screen.getByTestId('markdown')
|
|
expect(markdown).toHaveClass('line-clamp-2')
|
|
})
|
|
|
|
it('should apply line-clamp-20 when not fullDocMode and isCollapsed is false', () => {
|
|
mockDocForm.current = ChunkingMode.text
|
|
mockIsCollapsed.current = false
|
|
const detail = createMockSegmentDetail({
|
|
sign_content: 'Expanded content',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const markdown = screen.getByTestId('markdown')
|
|
expect(markdown).toHaveClass('line-clamp-20')
|
|
})
|
|
|
|
it('should fall back to content when sign_content is empty', () => {
|
|
const detail = createMockSegmentDetail({
|
|
content: 'Fallback content',
|
|
sign_content: '',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
expect(screen.getByText('Fallback content')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render empty string when both sign_content and content are empty', () => {
|
|
const detail = createMockSegmentDetail({
|
|
content: '',
|
|
sign_content: '',
|
|
})
|
|
|
|
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
|
|
|
|
const markdown = screen.getByTestId('markdown')
|
|
expect(markdown).toHaveTextContent('')
|
|
})
|
|
})
|
|
})
|