test: add unit tests for document title, new segment modal, CSV uploader, and various skeleton components in dataset documents detail

This commit is contained in:
CodingOnStar
2026-01-20 16:13:21 +08:00
parent d241a368c6
commit becaa9db0f
34 changed files with 7673 additions and 7 deletions

View File

@ -0,0 +1,242 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LanguagesSupported } from '@/i18n-config/language'
import { ChunkingMode } from '@/models/datasets'
import CSVDownload from './csv-downloader'
// Mock useLocale
let mockLocale = LanguagesSupported[0] // en-US
vi.mock('@/context/i18n', () => ({
useLocale: () => mockLocale,
}))
// Mock react-papaparse
const MockCSVDownloader = ({ children, data, filename, type }: { children: React.ReactNode, data: unknown, filename: string, type: string }) => (
<div
data-testid="csv-downloader-link"
data-filename={filename}
data-type={type}
data-data={JSON.stringify(data)}
>
{children}
</div>
)
vi.mock('react-papaparse', () => ({
useCSVDownloader: () => ({
CSVDownloader: MockCSVDownloader,
Type: { Link: 'link' },
}),
}))
describe('CSVDownloader', () => {
beforeEach(() => {
vi.clearAllMocks()
mockLocale = LanguagesSupported[0] // Reset to English
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render structure title', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert - i18n key format
expect(screen.getByText(/csvStructureTitle/i)).toBeInTheDocument()
})
it('should render download template link', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
expect(screen.getByTestId('csv-downloader-link')).toBeInTheDocument()
expect(screen.getByText(/list\.batchModal\.template/i)).toBeInTheDocument()
})
})
// Table structure for QA mode
describe('QA Mode Table', () => {
it('should render QA table with question and answer columns when docForm is qa', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.qa} />)
// Assert - Check for question/answer headers
const questionHeaders = screen.getAllByText(/list\.batchModal\.question/i)
const answerHeaders = screen.getAllByText(/list\.batchModal\.answer/i)
expect(questionHeaders.length).toBeGreaterThan(0)
expect(answerHeaders.length).toBeGreaterThan(0)
})
it('should render two data rows for QA mode', () => {
// Arrange & Act
const { container } = render(<CSVDownload docForm={ChunkingMode.qa} />)
// Assert
const tbody = container.querySelector('tbody')
expect(tbody).toBeInTheDocument()
const rows = tbody?.querySelectorAll('tr')
expect(rows?.length).toBe(2)
})
})
// Table structure for Text mode
describe('Text Mode Table', () => {
it('should render text table with content column when docForm is text', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert - Check for content header
expect(screen.getByText(/list\.batchModal\.contentTitle/i)).toBeInTheDocument()
})
it('should not render question/answer columns in text mode', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
expect(screen.queryByText(/list\.batchModal\.question/i)).not.toBeInTheDocument()
expect(screen.queryByText(/list\.batchModal\.answer/i)).not.toBeInTheDocument()
})
it('should render two data rows for text mode', () => {
// Arrange & Act
const { container } = render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
const tbody = container.querySelector('tbody')
expect(tbody).toBeInTheDocument()
const rows = tbody?.querySelectorAll('tr')
expect(rows?.length).toBe(2)
})
})
// CSV Template Data
describe('CSV Template Data', () => {
it('should provide English QA template when locale is English and docForm is qa', () => {
// Arrange
mockLocale = LanguagesSupported[0] // en-US
// Act
render(<CSVDownload docForm={ChunkingMode.qa} />)
// Assert
const link = screen.getByTestId('csv-downloader-link')
const data = JSON.parse(link.getAttribute('data-data') || '[]')
expect(data).toEqual([
['question', 'answer'],
['question1', 'answer1'],
['question2', 'answer2'],
])
})
it('should provide English text template when locale is English and docForm is text', () => {
// Arrange
mockLocale = LanguagesSupported[0] // en-US
// Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
const link = screen.getByTestId('csv-downloader-link')
const data = JSON.parse(link.getAttribute('data-data') || '[]')
expect(data).toEqual([
['segment content'],
['content1'],
['content2'],
])
})
it('should provide Chinese QA template when locale is Chinese and docForm is qa', () => {
// Arrange
mockLocale = LanguagesSupported[1] // zh-Hans
// Act
render(<CSVDownload docForm={ChunkingMode.qa} />)
// Assert
const link = screen.getByTestId('csv-downloader-link')
const data = JSON.parse(link.getAttribute('data-data') || '[]')
expect(data).toEqual([
['问题', '答案'],
['问题 1', '答案 1'],
['问题 2', '答案 2'],
])
})
it('should provide Chinese text template when locale is Chinese and docForm is text', () => {
// Arrange
mockLocale = LanguagesSupported[1] // zh-Hans
// Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
const link = screen.getByTestId('csv-downloader-link')
const data = JSON.parse(link.getAttribute('data-data') || '[]')
expect(data).toEqual([
['分段内容'],
['内容 1'],
['内容 2'],
])
})
})
// CSVDownloader props
describe('CSVDownloader Props', () => {
it('should set filename to template', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
const link = screen.getByTestId('csv-downloader-link')
expect(link.getAttribute('data-filename')).toBe('template')
})
it('should set type to Link', () => {
// Arrange & Act
render(<CSVDownload docForm={ChunkingMode.text} />)
// Assert
const link = screen.getByTestId('csv-downloader-link')
expect(link.getAttribute('data-type')).toBe('link')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered with different docForm', () => {
// Arrange
const { rerender } = render(<CSVDownload docForm={ChunkingMode.text} />)
// Act
rerender(<CSVDownload docForm={ChunkingMode.qa} />)
// Assert - should now show QA table
expect(screen.getAllByText(/list\.batchModal\.question/i).length).toBeGreaterThan(0)
})
it('should render correctly for non-English locales', () => {
// Arrange
mockLocale = LanguagesSupported[1] // zh-Hans
// Act
render(<CSVDownload docForm={ChunkingMode.qa} />)
// Assert - Check that Chinese template is used
const link = screen.getByTestId('csv-downloader-link')
const data = JSON.parse(link.getAttribute('data-data') || '[]')
expect(data[0]).toEqual(['问题', '答案'])
})
})
})

View File

@ -0,0 +1,368 @@
import type { CustomFile, FileItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Theme } from '@/types/app'
import CSVUploader from './csv-uploader'
// Mock upload service
const mockUpload = vi.fn()
vi.mock('@/service/base', () => ({
upload: (...args: unknown[]) => mockUpload(...args),
}))
// Mock useFileUploadConfig
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: () => ({
data: { file_size_limit: 15 },
}),
}))
// Mock useTheme
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: Theme.light }),
}))
// Mock ToastContext
const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
ToastContext: {
Provider: ({ children }: { children: React.ReactNode }) => children,
Consumer: ({ children }: { children: (ctx: { notify: typeof mockNotify }) => React.ReactNode }) => children({ notify: mockNotify }),
},
}))
// Create a mock ToastContext for useContext
vi.mock('use-context-selector', async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>
return {
...actual,
useContext: () => ({ notify: mockNotify }),
}
})
describe('CSVUploader', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const defaultProps = {
file: undefined as FileItem | undefined,
updateFile: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<CSVUploader {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render upload area when no file is present', () => {
// Arrange & Act
render(<CSVUploader {...defaultProps} />)
// Assert
expect(screen.getByText(/list\.batchModal\.csvUploadTitle/i)).toBeInTheDocument()
expect(screen.getByText(/list\.batchModal\.browse/i)).toBeInTheDocument()
})
it('should render hidden file input', () => {
// Arrange & Act
const { container } = render(<CSVUploader {...defaultProps} />)
// Assert
const fileInput = container.querySelector('input[type="file"]')
expect(fileInput).toBeInTheDocument()
expect(fileInput).toHaveStyle({ display: 'none' })
})
it('should accept only CSV files', () => {
// Arrange & Act
const { container } = render(<CSVUploader {...defaultProps} />)
// Assert
const fileInput = container.querySelector('input[type="file"]')
expect(fileInput).toHaveAttribute('accept', '.csv')
})
})
// File display tests
describe('File Display', () => {
it('should display file info when file is present', () => {
// Arrange
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test-file.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
// Act
render(<CSVUploader {...defaultProps} file={mockFile} />)
// Assert
expect(screen.getByText('test-file')).toBeInTheDocument()
expect(screen.getByText('.csv')).toBeInTheDocument()
})
it('should not show upload area when file is present', () => {
// Arrange
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
// Act
render(<CSVUploader {...defaultProps} file={mockFile} />)
// Assert
expect(screen.queryByText(/list\.batchModal\.csvUploadTitle/i)).not.toBeInTheDocument()
})
it('should show change button when file is present', () => {
// Arrange
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
// Act
render(<CSVUploader {...defaultProps} file={mockFile} />)
// Assert
expect(screen.getByText(/stepOne\.uploader\.change/i)).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should trigger file input click when browse is clicked', () => {
// Arrange
const { container } = render(<CSVUploader {...defaultProps} />)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
const clickSpy = vi.spyOn(fileInput, 'click')
// Act
fireEvent.click(screen.getByText(/list\.batchModal\.browse/i))
// Assert
expect(clickSpy).toHaveBeenCalled()
})
it('should call updateFile when file is selected', async () => {
// Arrange
const mockUpdateFile = vi.fn()
mockUpload.mockResolvedValueOnce({ id: 'uploaded-id' })
const { container } = render(
<CSVUploader {...defaultProps} updateFile={mockUpdateFile} />,
)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
const testFile = new File(['content'], 'test.csv', { type: 'text/csv' })
// Act
fireEvent.change(fileInput, { target: { files: [testFile] } })
// Assert
await waitFor(() => {
expect(mockUpdateFile).toHaveBeenCalled()
})
})
it('should call updateFile with undefined when remove is clicked', () => {
// Arrange
const mockUpdateFile = vi.fn()
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
const { container } = render(
<CSVUploader {...defaultProps} file={mockFile} updateFile={mockUpdateFile} />,
)
// Act
const deleteButton = container.querySelector('.cursor-pointer')
if (deleteButton)
fireEvent.click(deleteButton)
// Assert
expect(mockUpdateFile).toHaveBeenCalledWith()
})
})
// Validation tests
describe('Validation', () => {
it('should show error for non-CSV files', () => {
// Arrange
const { container } = render(<CSVUploader {...defaultProps} />)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
const testFile = new File(['content'], 'test.txt', { type: 'text/plain' })
// Act
fireEvent.change(fileInput, { target: { files: [testFile] } })
// Assert
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
it('should show error for files exceeding size limit', () => {
// Arrange
const { container } = render(<CSVUploader {...defaultProps} />)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
// Create a mock file with a large size (16MB) without actually creating the data
const testFile = new File(['test'], 'large.csv', { type: 'text/csv' })
Object.defineProperty(testFile, 'size', { value: 16 * 1024 * 1024 })
// Act
fireEvent.change(fileInput, { target: { files: [testFile] } })
// Assert
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
})
// Upload progress tests
describe('Upload Progress', () => {
it('should show progress indicator when upload is in progress', () => {
// Arrange
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
progress: 50,
}
// Act
const { container } = render(<CSVUploader {...defaultProps} file={mockFile} />)
// Assert - SimplePieChart should be rendered for progress 0-99
// The pie chart would be in the hidden group element
expect(container.querySelector('.group')).toBeInTheDocument()
})
it('should not show progress for completed uploads', () => {
// Arrange
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'test.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
// Act
render(<CSVUploader {...defaultProps} file={mockFile} />)
// Assert - File name should be displayed
expect(screen.getByText('test')).toBeInTheDocument()
})
})
// Props tests
describe('Props', () => {
it('should call updateFile prop when provided', async () => {
// Arrange
const mockUpdateFile = vi.fn()
mockUpload.mockResolvedValueOnce({ id: 'test-id' })
const { container } = render(
<CSVUploader file={undefined} updateFile={mockUpdateFile} />,
)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
const testFile = new File(['content'], 'test.csv', { type: 'text/csv' })
// Act
fireEvent.change(fileInput, { target: { files: [testFile] } })
// Assert
await waitFor(() => {
expect(mockUpdateFile).toHaveBeenCalled()
})
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty file list', () => {
// Arrange
const mockUpdateFile = vi.fn()
const { container } = render(
<CSVUploader {...defaultProps} updateFile={mockUpdateFile} />,
)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
// Act
fireEvent.change(fileInput, { target: { files: [] } })
// Assert
expect(mockUpdateFile).not.toHaveBeenCalled()
})
it('should handle null file', () => {
// Arrange
const mockUpdateFile = vi.fn()
const { container } = render(
<CSVUploader {...defaultProps} updateFile={mockUpdateFile} />,
)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
// Act
fireEvent.change(fileInput, { target: { files: null } })
// Assert
expect(mockUpdateFile).not.toHaveBeenCalled()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<CSVUploader {...defaultProps} />)
// Act
const mockFile: FileItem = {
fileID: 'file-1',
file: new File(['content'], 'updated.csv', { type: 'text/csv' }) as CustomFile,
progress: 100,
}
rerender(<CSVUploader {...defaultProps} file={mockFile} />)
// Assert
expect(screen.getByText('updated')).toBeInTheDocument()
})
it('should handle upload error', async () => {
// Arrange
const mockUpdateFile = vi.fn()
mockUpload.mockRejectedValueOnce(new Error('Upload failed'))
const { container } = render(
<CSVUploader {...defaultProps} updateFile={mockUpdateFile} />,
)
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
const testFile = new File(['content'], 'test.csv', { type: 'text/csv' })
// Act
fireEvent.change(fileInput, { target: { files: [testFile] } })
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
})
})
})

View File

@ -0,0 +1,213 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import BatchModal from './index'
// Mock child components
vi.mock('./csv-downloader', () => ({
default: ({ docForm }: { docForm: ChunkingMode }) => (
<div data-testid="csv-downloader" data-doc-form={docForm}>
CSV Downloader
</div>
),
}))
vi.mock('./csv-uploader', () => ({
default: ({ file, updateFile }: { file: { file?: { id: string } } | undefined, updateFile: (file: { file: { id: string } } | undefined) => void }) => (
<div data-testid="csv-uploader">
<button
data-testid="upload-btn"
onClick={() => updateFile({ file: { id: 'test-file-id' } })}
>
Upload
</button>
<button
data-testid="clear-btn"
onClick={() => updateFile(undefined)}
>
Clear
</button>
{file && <span data-testid="file-info">{file.file?.id}</span>}
</div>
),
}))
describe('BatchModal', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const defaultProps = {
isShow: true,
docForm: ChunkingMode.text,
onCancel: vi.fn(),
onConfirm: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing when isShow is true', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} />)
// Assert
expect(screen.getByText(/list\.batchModal\.title/i)).toBeInTheDocument()
})
it('should not render content when isShow is false', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} isShow={false} />)
// Assert - Modal is closed
expect(screen.queryByText(/list\.batchModal\.title/i)).not.toBeInTheDocument()
})
it('should render CSVDownloader component', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('csv-downloader')).toBeInTheDocument()
})
it('should render CSVUploader component', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('csv-uploader')).toBeInTheDocument()
})
it('should render cancel and run buttons', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} />)
// Assert
expect(screen.getByText(/list\.batchModal\.cancel/i)).toBeInTheDocument()
expect(screen.getByText(/list\.batchModal\.run/i)).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCancel when cancel button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
render(<BatchModal {...defaultProps} onCancel={mockOnCancel} />)
// Act
fireEvent.click(screen.getByText(/list\.batchModal\.cancel/i))
// Assert
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
it('should disable run button when no file is uploaded', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} />)
// Assert
const runButton = screen.getByText(/list\.batchModal\.run/i).closest('button')
expect(runButton).toBeDisabled()
})
it('should enable run button after file is uploaded', async () => {
// Arrange
render(<BatchModal {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('upload-btn'))
// Assert
await waitFor(() => {
const runButton = screen.getByText(/list\.batchModal\.run/i).closest('button')
expect(runButton).not.toBeDisabled()
})
})
it('should call onConfirm with file when run button is clicked', async () => {
// Arrange
const mockOnConfirm = vi.fn()
const mockOnCancel = vi.fn()
render(<BatchModal {...defaultProps} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
// Act - upload file first
fireEvent.click(screen.getByTestId('upload-btn'))
await waitFor(() => {
const runButton = screen.getByText(/list\.batchModal\.run/i).closest('button')
expect(runButton).not.toBeDisabled()
})
// Act - click run
fireEvent.click(screen.getByText(/list\.batchModal\.run/i))
// Assert
expect(mockOnCancel).toHaveBeenCalledTimes(1)
expect(mockOnConfirm).toHaveBeenCalledWith({ file: { id: 'test-file-id' } })
})
})
// Props tests
describe('Props', () => {
it('should pass docForm to CSVDownloader', () => {
// Arrange & Act
render(<BatchModal {...defaultProps} docForm={ChunkingMode.qa} />)
// Assert
expect(screen.getByTestId('csv-downloader').getAttribute('data-doc-form')).toBe(ChunkingMode.qa)
})
})
// State reset tests
describe('State Reset', () => {
it('should reset file when modal is closed and reopened', async () => {
// Arrange
const { rerender } = render(<BatchModal {...defaultProps} />)
// Upload a file
fireEvent.click(screen.getByTestId('upload-btn'))
await waitFor(() => {
expect(screen.getByTestId('file-info')).toBeInTheDocument()
})
// Close modal
rerender(<BatchModal {...defaultProps} isShow={false} />)
// Reopen modal
rerender(<BatchModal {...defaultProps} isShow={true} />)
// Assert - file should be cleared
expect(screen.queryByTestId('file-info')).not.toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should not call onConfirm when no file is present', () => {
// Arrange
const mockOnConfirm = vi.fn()
render(<BatchModal {...defaultProps} onConfirm={mockOnConfirm} />)
// Act - try to click run (should be disabled)
const runButton = screen.getByText(/list\.batchModal\.run/i).closest('button')
if (runButton)
fireEvent.click(runButton)
// Assert
expect(mockOnConfirm).not.toHaveBeenCalled()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<BatchModal {...defaultProps} />)
// Act
rerender(<BatchModal {...defaultProps} docForm={ChunkingMode.qa} />)
// Assert
expect(screen.getByText(/list\.batchModal\.title/i)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,292 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import ChildSegmentDetail from './child-segment-detail'
// Mock segment list context
let mockFullScreen = false
const mockToggleFullScreen = vi.fn()
vi.mock('./index', () => ({
useSegmentListContext: (selector: (state: { fullScreen: boolean, toggleFullScreen: () => void }) => unknown) => {
const state = {
fullScreen: mockFullScreen,
toggleFullScreen: mockToggleFullScreen,
}
return selector(state)
},
}))
// Mock event emitter context
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: {
useSubscription: vi.fn(),
},
}),
}))
// Mock child components
vi.mock('./common/action-buttons', () => ({
default: ({ handleCancel, handleSave, loading, isChildChunk }: { handleCancel: () => void, handleSave: () => void, loading: boolean, isChildChunk?: boolean }) => (
<div data-testid="action-buttons">
<button onClick={handleCancel} data-testid="cancel-btn">Cancel</button>
<button onClick={handleSave} disabled={loading} data-testid="save-btn">Save</button>
<span data-testid="is-child-chunk">{isChildChunk ? 'true' : 'false'}</span>
</div>
),
}))
vi.mock('./common/chunk-content', () => ({
default: ({ question, onQuestionChange, isEditMode }: { question: string, onQuestionChange: (v: string) => void, isEditMode: boolean }) => (
<div data-testid="chunk-content">
<input
data-testid="content-input"
value={question}
onChange={e => onQuestionChange(e.target.value)}
/>
<span data-testid="edit-mode">{isEditMode ? 'editing' : 'viewing'}</span>
</div>
),
}))
vi.mock('./common/dot', () => ({
default: () => <span data-testid="dot"></span>,
}))
vi.mock('./common/segment-index-tag', () => ({
SegmentIndexTag: ({ positionId, labelPrefix }: { positionId?: string, labelPrefix?: string }) => (
<span data-testid="segment-index-tag">
{labelPrefix}
{' '}
{positionId}
</span>
),
}))
describe('ChildSegmentDetail', () => {
beforeEach(() => {
vi.clearAllMocks()
mockFullScreen = false
})
const defaultChildChunkInfo = {
id: 'child-chunk-1',
content: 'Test content',
position: 1,
updated_at: 1609459200, // 2021-01-01
}
const defaultProps = {
chunkId: 'chunk-1',
childChunkInfo: defaultChildChunkInfo,
onUpdate: vi.fn(),
onCancel: vi.fn(),
docForm: ChunkingMode.text,
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render edit child chunk title', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByText(/segment\.editChildChunk/i)).toBeInTheDocument()
})
it('should render chunk content component', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('chunk-content')).toBeInTheDocument()
})
it('should render segment index tag', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('segment-index-tag')).toBeInTheDocument()
})
it('should render word count', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByText(/segment\.characters/i)).toBeInTheDocument()
})
it('should render edit time', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByText(/segment\.editedAt/i)).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCancel when close button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
const { container } = render(
<ChildSegmentDetail {...defaultProps} onCancel={mockOnCancel} />,
)
// Act
const closeButtons = container.querySelectorAll('.cursor-pointer')
if (closeButtons.length > 1)
fireEvent.click(closeButtons[1])
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
it('should call toggleFullScreen when expand button is clicked', () => {
// Arrange
const { container } = render(<ChildSegmentDetail {...defaultProps} />)
// Act
const expandButtons = container.querySelectorAll('.cursor-pointer')
if (expandButtons.length > 0)
fireEvent.click(expandButtons[0])
// Assert
expect(mockToggleFullScreen).toHaveBeenCalled()
})
it('should call onUpdate when save is clicked', () => {
// Arrange
const mockOnUpdate = vi.fn()
render(<ChildSegmentDetail {...defaultProps} onUpdate={mockOnUpdate} />)
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
expect(mockOnUpdate).toHaveBeenCalledWith(
'chunk-1',
'child-chunk-1',
'Test content',
)
})
it('should update content when input changes', () => {
// Arrange
render(<ChildSegmentDetail {...defaultProps} />)
// Act
fireEvent.change(screen.getByTestId('content-input'), {
target: { value: 'Updated content' },
})
// Assert
expect(screen.getByTestId('content-input')).toHaveValue('Updated content')
})
})
// Full screen mode
describe('Full Screen Mode', () => {
it('should show action buttons in header when fullScreen is true', () => {
// Arrange
mockFullScreen = true
// Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
it('should not show footer action buttons when fullScreen is true', () => {
// Arrange
mockFullScreen = true
// Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert - footer with border-t-divider-subtle should not exist
const actionButtons = screen.getAllByTestId('action-buttons')
// Only one action buttons set should exist in fullScreen mode
expect(actionButtons.length).toBe(1)
})
it('should show footer action buttons when fullScreen is false', () => {
// Arrange
mockFullScreen = false
// Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
})
// Props
describe('Props', () => {
it('should pass isChildChunk true to ActionButtons', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('is-child-chunk')).toHaveTextContent('true')
})
it('should pass isEditMode true to ChunkContent', () => {
// Arrange & Act
render(<ChildSegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('edit-mode')).toHaveTextContent('editing')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle undefined childChunkInfo', () => {
// Arrange & Act
const { container } = render(
<ChildSegmentDetail {...defaultProps} childChunkInfo={undefined} />,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle empty content', () => {
// Arrange
const emptyChildChunkInfo = { ...defaultChildChunkInfo, content: '' }
// Act
render(<ChildSegmentDetail {...defaultProps} childChunkInfo={emptyChildChunkInfo} />)
// Assert
expect(screen.getByTestId('content-input')).toHaveValue('')
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<ChildSegmentDetail {...defaultProps} />)
// Act
const updatedInfo = { ...defaultChildChunkInfo, content: 'New content' }
rerender(<ChildSegmentDetail {...defaultProps} childChunkInfo={updatedInfo} />)
// Assert
expect(screen.getByTestId('content-input')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,386 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { DocumentContext } from '../../context'
import ActionButtons from './action-buttons'
// Create wrapper component for providing context
const createWrapper = (contextValue: {
docForm?: ChunkingMode
parentMode?: 'paragraph' | 'full-doc'
}) => {
return ({ children }: { children: React.ReactNode }) => (
<DocumentContext.Provider value={contextValue}>
{children}
</DocumentContext.Provider>
)
}
describe('ActionButtons', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render cancel button', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Assert
expect(screen.getByText(/operation\.cancel/i)).toBeInTheDocument()
})
it('should render save button', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Assert
expect(screen.getByText(/operation\.save/i)).toBeInTheDocument()
})
it('should render ESC keyboard hint on cancel button', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Assert
expect(screen.getByText('ESC')).toBeInTheDocument()
})
it('should render S keyboard hint on save button', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Assert
expect(screen.getByText('S')).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call handleCancel when cancel button is clicked', () => {
// Arrange
const mockHandleCancel = vi.fn()
render(
<ActionButtons
handleCancel={mockHandleCancel}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Act
const cancelButton = screen.getAllByRole('button')[0]
fireEvent.click(cancelButton)
// Assert
expect(mockHandleCancel).toHaveBeenCalledTimes(1)
})
it('should call handleSave when save button is clicked', () => {
// Arrange
const mockHandleSave = vi.fn()
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={mockHandleSave}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Act
const buttons = screen.getAllByRole('button')
const saveButton = buttons[buttons.length - 1] // Save button is last
fireEvent.click(saveButton)
// Assert
expect(mockHandleSave).toHaveBeenCalledTimes(1)
})
it('should disable save button when loading is true', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={true}
/>,
{ wrapper: createWrapper({}) },
)
// Assert
const buttons = screen.getAllByRole('button')
const saveButton = buttons[buttons.length - 1]
expect(saveButton).toBeDisabled()
})
})
// Regeneration button tests
describe('Regeneration Button', () => {
it('should show regeneration button in parent-child paragraph mode for edit action', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
actionType="edit"
isChildChunk={false}
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert
expect(screen.getByText(/operation\.saveAndRegenerate/i)).toBeInTheDocument()
})
it('should not show regeneration button when isChildChunk is true', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
actionType="edit"
isChildChunk={true}
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert
expect(screen.queryByText(/operation\.saveAndRegenerate/i)).not.toBeInTheDocument()
})
it('should not show regeneration button when showRegenerationButton is false', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
actionType="edit"
isChildChunk={false}
showRegenerationButton={false}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert
expect(screen.queryByText(/operation\.saveAndRegenerate/i)).not.toBeInTheDocument()
})
it('should not show regeneration button when actionType is add', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
actionType="add"
isChildChunk={false}
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert
expect(screen.queryByText(/operation\.saveAndRegenerate/i)).not.toBeInTheDocument()
})
it('should call handleRegeneration when regeneration button is clicked', () => {
// Arrange
const mockHandleRegeneration = vi.fn()
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={mockHandleRegeneration}
loading={false}
actionType="edit"
isChildChunk={false}
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Act
const regenerationButton = screen.getByText(/operation\.saveAndRegenerate/i).closest('button')
if (regenerationButton)
fireEvent.click(regenerationButton)
// Assert
expect(mockHandleRegeneration).toHaveBeenCalledTimes(1)
})
it('should disable regeneration button when loading is true', () => {
// Arrange & Act
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={true}
actionType="edit"
isChildChunk={false}
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert
const regenerationButton = screen.getByText(/operation\.saveAndRegenerate/i).closest('button')
expect(regenerationButton).toBeDisabled()
})
})
// Default props tests
describe('Default Props', () => {
it('should use default actionType of edit', () => {
// Arrange & Act - when not specifying actionType and other conditions are met
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert - regeneration button should show with default actionType='edit'
expect(screen.getByText(/operation\.saveAndRegenerate/i)).toBeInTheDocument()
})
it('should use default isChildChunk of false', () => {
// Arrange & Act - when not specifying isChildChunk
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
actionType="edit"
showRegenerationButton={true}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert - regeneration button should show with default isChildChunk=false
expect(screen.getByText(/operation\.saveAndRegenerate/i)).toBeInTheDocument()
})
it('should use default showRegenerationButton of true', () => {
// Arrange & Act - when not specifying showRegenerationButton
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
handleRegeneration={vi.fn()}
loading={false}
actionType="edit"
isChildChunk={false}
/>,
{ wrapper: createWrapper({ docForm: ChunkingMode.parentChild, parentMode: 'paragraph' }) },
)
// Assert - regeneration button should show with default showRegenerationButton=true
expect(screen.getByText(/operation\.saveAndRegenerate/i)).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle missing context values gracefully', () => {
// Arrange & Act & Assert - should not throw
expect(() => {
render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
}).not.toThrow()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={false}
/>,
{ wrapper: createWrapper({}) },
)
// Act
rerender(
<DocumentContext.Provider value={{}}>
<ActionButtons
handleCancel={vi.fn()}
handleSave={vi.fn()}
loading={true}
/>
</DocumentContext.Provider>,
)
// Assert
expect(screen.getByText(/operation\.cancel/i)).toBeInTheDocument()
expect(screen.getByText(/operation\.save/i)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,194 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AddAnother from './add-another'
describe('AddAnother', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the checkbox', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
)
// Assert - Checkbox component renders with shrink-0 class
const checkbox = container.querySelector('.shrink-0')
expect(checkbox).toBeInTheDocument()
})
it('should render the add another text', () => {
// Arrange & Act
render(<AddAnother isChecked={false} onCheck={vi.fn()} />)
// Assert - i18n key format
expect(screen.getByText(/segment\.addAnother/i)).toBeInTheDocument()
})
it('should render with correct base styling classes', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex')
expect(wrapper).toHaveClass('items-center')
expect(wrapper).toHaveClass('gap-x-1')
expect(wrapper).toHaveClass('pl-1')
})
})
// Props tests
describe('Props', () => {
it('should render unchecked state when isChecked is false', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
)
// Assert - unchecked checkbox has border class
const checkbox = container.querySelector('.border-components-checkbox-border')
expect(checkbox).toBeInTheDocument()
})
it('should render checked state when isChecked is true', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={true} onCheck={vi.fn()} />,
)
// Assert - checked checkbox has bg-components-checkbox-bg class
const checkbox = container.querySelector('.bg-components-checkbox-bg')
expect(checkbox).toBeInTheDocument()
})
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<AddAnother
isChecked={false}
onCheck={vi.fn()}
className="custom-class"
/>,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCheck when checkbox is clicked', () => {
// Arrange
const mockOnCheck = vi.fn()
const { container } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
)
// Act - click on the checkbox element
const checkbox = container.querySelector('.shrink-0')
if (checkbox)
fireEvent.click(checkbox)
// Assert
expect(mockOnCheck).toHaveBeenCalledTimes(1)
})
it('should toggle checked state on multiple clicks', () => {
// Arrange
const mockOnCheck = vi.fn()
const { container, rerender } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
)
// Act - first click
const checkbox = container.querySelector('.shrink-0')
if (checkbox) {
fireEvent.click(checkbox)
rerender(<AddAnother isChecked={true} onCheck={mockOnCheck} />)
fireEvent.click(checkbox)
}
// Assert
expect(mockOnCheck).toHaveBeenCalledTimes(2)
})
})
// Structure tests
describe('Structure', () => {
it('should render text with tertiary text color', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
)
// Assert
const textElement = container.querySelector('.text-text-tertiary')
expect(textElement).toBeInTheDocument()
})
it('should render text with xs medium font styling', () => {
// Arrange & Act
const { container } = render(
<AddAnother isChecked={false} onCheck={vi.fn()} />,
)
// Assert
const textElement = container.querySelector('.system-xs-medium')
expect(textElement).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
// Arrange
const mockOnCheck = vi.fn()
const { rerender, container } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
)
// Act
rerender(<AddAnother isChecked={true} onCheck={mockOnCheck} />)
// Assert
const checkbox = container.querySelector('.shrink-0')
expect(checkbox).toBeInTheDocument()
})
it('should handle rapid state changes', () => {
// Arrange
const mockOnCheck = vi.fn()
const { container } = render(
<AddAnother isChecked={false} onCheck={mockOnCheck} />,
)
// Act
const checkbox = container.querySelector('.shrink-0')
if (checkbox) {
for (let i = 0; i < 5; i++)
fireEvent.click(checkbox)
}
// Assert
expect(mockOnCheck).toHaveBeenCalledTimes(5)
})
})
})

View File

@ -0,0 +1,277 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import BatchAction from './batch-action'
describe('BatchAction', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const defaultProps = {
selectedIds: ['1', '2', '3'],
onBatchEnable: vi.fn(),
onBatchDisable: vi.fn(),
onBatchDelete: vi.fn().mockResolvedValue(undefined),
onCancel: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<BatchAction {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should display selected count', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} />)
// Assert
expect(screen.getByText('3')).toBeInTheDocument()
})
it('should render enable button', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} />)
// Assert
expect(screen.getByText(/batchAction\.enable/i)).toBeInTheDocument()
})
it('should render disable button', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} />)
// Assert
expect(screen.getByText(/batchAction\.disable/i)).toBeInTheDocument()
})
it('should render delete button', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} />)
// Assert
expect(screen.getByText(/batchAction\.delete/i)).toBeInTheDocument()
})
it('should render cancel button', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} />)
// Assert
expect(screen.getByText(/batchAction\.cancel/i)).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onBatchEnable when enable button is clicked', () => {
// Arrange
const mockOnBatchEnable = vi.fn()
render(<BatchAction {...defaultProps} onBatchEnable={mockOnBatchEnable} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.enable/i))
// Assert
expect(mockOnBatchEnable).toHaveBeenCalledTimes(1)
})
it('should call onBatchDisable when disable button is clicked', () => {
// Arrange
const mockOnBatchDisable = vi.fn()
render(<BatchAction {...defaultProps} onBatchDisable={mockOnBatchDisable} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.disable/i))
// Assert
expect(mockOnBatchDisable).toHaveBeenCalledTimes(1)
})
it('should call onCancel when cancel button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
render(<BatchAction {...defaultProps} onCancel={mockOnCancel} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.cancel/i))
// Assert
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
it('should show delete confirmation dialog when delete button is clicked', () => {
// Arrange
render(<BatchAction {...defaultProps} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.delete/i))
// Assert - Confirm dialog should appear
expect(screen.getByText(/list\.delete\.title/i)).toBeInTheDocument()
})
it('should call onBatchDelete when confirm is clicked in delete dialog', async () => {
// Arrange
const mockOnBatchDelete = vi.fn().mockResolvedValue(undefined)
render(<BatchAction {...defaultProps} onBatchDelete={mockOnBatchDelete} />)
// Act - open delete dialog
fireEvent.click(screen.getByText(/batchAction\.delete/i))
// Act - click confirm
const confirmButton = screen.getByText(/operation\.sure/i)
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockOnBatchDelete).toHaveBeenCalledTimes(1)
})
})
})
// Optional props tests
describe('Optional Props', () => {
it('should render download button when onBatchDownload is provided', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} onBatchDownload={vi.fn()} />)
// Assert
expect(screen.getByText(/batchAction\.download/i)).toBeInTheDocument()
})
it('should not render download button when onBatchDownload is not provided', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} />)
// Assert
expect(screen.queryByText(/batchAction\.download/i)).not.toBeInTheDocument()
})
it('should render archive button when onArchive is provided', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} onArchive={vi.fn()} />)
// Assert
expect(screen.getByText(/batchAction\.archive/i)).toBeInTheDocument()
})
it('should render metadata button when onEditMetadata is provided', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} onEditMetadata={vi.fn()} />)
// Assert
expect(screen.getByText(/metadata\.metadata/i)).toBeInTheDocument()
})
it('should render re-index button when onBatchReIndex is provided', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} onBatchReIndex={vi.fn()} />)
// Assert
expect(screen.getByText(/batchAction\.reIndex/i)).toBeInTheDocument()
})
it('should call onBatchDownload when download button is clicked', () => {
// Arrange
const mockOnBatchDownload = vi.fn()
render(<BatchAction {...defaultProps} onBatchDownload={mockOnBatchDownload} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.download/i))
// Assert
expect(mockOnBatchDownload).toHaveBeenCalledTimes(1)
})
it('should call onArchive when archive button is clicked', () => {
// Arrange
const mockOnArchive = vi.fn()
render(<BatchAction {...defaultProps} onArchive={mockOnArchive} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.archive/i))
// Assert
expect(mockOnArchive).toHaveBeenCalledTimes(1)
})
it('should call onEditMetadata when metadata button is clicked', () => {
// Arrange
const mockOnEditMetadata = vi.fn()
render(<BatchAction {...defaultProps} onEditMetadata={mockOnEditMetadata} />)
// Act
fireEvent.click(screen.getByText(/metadata\.metadata/i))
// Assert
expect(mockOnEditMetadata).toHaveBeenCalledTimes(1)
})
it('should call onBatchReIndex when re-index button is clicked', () => {
// Arrange
const mockOnBatchReIndex = vi.fn()
render(<BatchAction {...defaultProps} onBatchReIndex={mockOnBatchReIndex} />)
// Act
fireEvent.click(screen.getByText(/batchAction\.reIndex/i))
// Assert
expect(mockOnBatchReIndex).toHaveBeenCalledTimes(1)
})
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(<BatchAction {...defaultProps} className="custom-class" />)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
})
})
// Selected count display tests
describe('Selected Count', () => {
it('should display correct count for single selection', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} selectedIds={['1']} />)
// Assert
expect(screen.getByText('1')).toBeInTheDocument()
})
it('should display correct count for multiple selections', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} selectedIds={['1', '2', '3', '4', '5']} />)
// Assert
expect(screen.getByText('5')).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<BatchAction {...defaultProps} />)
// Act
rerender(<BatchAction {...defaultProps} selectedIds={['1', '2']} />)
// Assert
expect(screen.getByText('2')).toBeInTheDocument()
})
it('should handle empty selectedIds array', () => {
// Arrange & Act
render(<BatchAction {...defaultProps} selectedIds={[]} />)
// Assert
expect(screen.getByText('0')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,309 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import ChunkContent from './chunk-content'
// Mock ResizeObserver
class MockResizeObserver {
observe = vi.fn()
disconnect = vi.fn()
unobserve = vi.fn()
}
globalThis.ResizeObserver = MockResizeObserver as typeof ResizeObserver
describe('ChunkContent', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const defaultProps = {
question: 'Test question content',
onQuestionChange: vi.fn(),
docForm: ChunkingMode.text,
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<ChunkContent {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render textarea in edit mode with text docForm', () => {
// Arrange & Act
render(<ChunkContent {...defaultProps} isEditMode={true} />)
// Assert
const textarea = screen.getByRole('textbox')
expect(textarea).toBeInTheDocument()
})
it('should render Markdown content in view mode with text docForm', () => {
// Arrange & Act
const { container } = render(<ChunkContent {...defaultProps} isEditMode={false} />)
// Assert - In view mode, textarea should not be present, Markdown renders instead
expect(container.querySelector('textarea')).not.toBeInTheDocument()
})
})
// QA mode tests
describe('QA Mode', () => {
it('should render QA layout when docForm is qa', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
answer="Test answer"
onAnswerChange={vi.fn()}
isEditMode={true}
/>,
)
// Assert - QA mode has QUESTION and ANSWER labels
expect(screen.getByText('QUESTION')).toBeInTheDocument()
expect(screen.getByText('ANSWER')).toBeInTheDocument()
})
it('should display question value in QA mode', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
question="My question"
answer="My answer"
onAnswerChange={vi.fn()}
isEditMode={true}
/>,
)
// Assert
const textareas = screen.getAllByRole('textbox')
expect(textareas[0]).toHaveValue('My question')
})
it('should display answer value in QA mode', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
question="My question"
answer="My answer"
onAnswerChange={vi.fn()}
isEditMode={true}
/>,
)
// Assert
const textareas = screen.getAllByRole('textbox')
expect(textareas[1]).toHaveValue('My answer')
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onQuestionChange when textarea value changes in text mode', () => {
// Arrange
const mockOnQuestionChange = vi.fn()
render(
<ChunkContent
{...defaultProps}
isEditMode={true}
onQuestionChange={mockOnQuestionChange}
/>,
)
// Act
const textarea = screen.getByRole('textbox')
fireEvent.change(textarea, { target: { value: 'New content' } })
// Assert
expect(mockOnQuestionChange).toHaveBeenCalledWith('New content')
})
it('should call onQuestionChange when question textarea changes in QA mode', () => {
// Arrange
const mockOnQuestionChange = vi.fn()
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
isEditMode={true}
onQuestionChange={mockOnQuestionChange}
onAnswerChange={vi.fn()}
/>,
)
// Act
const textareas = screen.getAllByRole('textbox')
fireEvent.change(textareas[0], { target: { value: 'New question' } })
// Assert
expect(mockOnQuestionChange).toHaveBeenCalledWith('New question')
})
it('should call onAnswerChange when answer textarea changes in QA mode', () => {
// Arrange
const mockOnAnswerChange = vi.fn()
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
isEditMode={true}
answer="Old answer"
onAnswerChange={mockOnAnswerChange}
/>,
)
// Act
const textareas = screen.getAllByRole('textbox')
fireEvent.change(textareas[1], { target: { value: 'New answer' } })
// Assert
expect(mockOnAnswerChange).toHaveBeenCalledWith('New answer')
})
it('should disable textarea when isEditMode is false in text mode', () => {
// Arrange & Act
const { container } = render(
<ChunkContent {...defaultProps} isEditMode={false} />,
)
// Assert - In view mode, Markdown is rendered instead of textarea
expect(container.querySelector('textarea')).not.toBeInTheDocument()
})
it('should disable textareas when isEditMode is false in QA mode', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
isEditMode={false}
answer="Answer"
onAnswerChange={vi.fn()}
/>,
)
// Assert
const textareas = screen.getAllByRole('textbox')
textareas.forEach((textarea) => {
expect(textarea).toBeDisabled()
})
})
})
// DocForm variations
describe('DocForm Variations', () => {
it('should handle ChunkingMode.text', () => {
// Arrange & Act
render(<ChunkContent {...defaultProps} docForm={ChunkingMode.text} isEditMode={true} />)
// Assert
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should handle ChunkingMode.qa', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
answer="answer"
onAnswerChange={vi.fn()}
isEditMode={true}
/>,
)
// Assert - QA mode should show both question and answer
expect(screen.getByText('QUESTION')).toBeInTheDocument()
expect(screen.getByText('ANSWER')).toBeInTheDocument()
})
it('should handle ChunkingMode.parentChild similar to text mode', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.parentChild}
isEditMode={true}
/>,
)
// Assert - parentChild should render like text mode
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty question', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
question=""
isEditMode={true}
/>,
)
// Assert
const textarea = screen.getByRole('textbox')
expect(textarea).toHaveValue('')
})
it('should handle empty answer in QA mode', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
question="question"
answer=""
onAnswerChange={vi.fn()}
isEditMode={true}
/>,
)
// Assert
const textareas = screen.getAllByRole('textbox')
expect(textareas[1]).toHaveValue('')
})
it('should handle undefined answer in QA mode', () => {
// Arrange & Act
render(
<ChunkContent
{...defaultProps}
docForm={ChunkingMode.qa}
isEditMode={true}
/>,
)
// Assert - should render without crashing
expect(screen.getByText('QUESTION')).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(
<ChunkContent {...defaultProps} question="Initial" isEditMode={true} />,
)
// Act
rerender(
<ChunkContent {...defaultProps} question="Updated" isEditMode={true} />,
)
// Assert
const textarea = screen.getByRole('textbox')
expect(textarea).toHaveValue('Updated')
})
})
})

View File

@ -0,0 +1,60 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Dot from './dot'
describe('Dot', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<Dot />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the dot character', () => {
// Arrange & Act
render(<Dot />)
// Assert
expect(screen.getByText('·')).toBeInTheDocument()
})
it('should render with correct styling classes', () => {
// Arrange & Act
const { container } = render(<Dot />)
// Assert
const dotElement = container.firstChild as HTMLElement
expect(dotElement).toHaveClass('system-xs-medium')
expect(dotElement).toHaveClass('text-text-quaternary')
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently across multiple renders', () => {
// Arrange & Act
const { container: container1 } = render(<Dot />)
const { container: container2 } = render(<Dot />)
// Assert
expect(container1.firstChild?.textContent).toBe(container2.firstChild?.textContent)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<Dot />)
// Act
rerender(<Dot />)
// Assert
expect(screen.getByText('·')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,153 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Empty from './empty'
describe('Empty', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<Empty onClearFilter={vi.fn()} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the file list icon', () => {
// Arrange & Act
const { container } = render(<Empty onClearFilter={vi.fn()} />)
// Assert - RiFileList2Line icon should be rendered
const icon = container.querySelector('.h-6.w-6')
expect(icon).toBeInTheDocument()
})
it('should render empty message text', () => {
// Arrange & Act
render(<Empty onClearFilter={vi.fn()} />)
// Assert - i18n key format: datasetDocuments:segment.empty
expect(screen.getByText(/segment\.empty/i)).toBeInTheDocument()
})
it('should render clear filter button', () => {
// Arrange & Act
render(<Empty onClearFilter={vi.fn()} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render background empty cards', () => {
// Arrange & Act
const { container } = render(<Empty onClearFilter={vi.fn()} />)
// Assert - should have 10 background cards
const emptyCards = container.querySelectorAll('.bg-background-section-burn')
expect(emptyCards).toHaveLength(10)
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onClearFilter when clear filter button is clicked', () => {
// Arrange
const mockOnClearFilter = vi.fn()
render(<Empty onClearFilter={mockOnClearFilter} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnClearFilter).toHaveBeenCalledTimes(1)
})
})
// Structure tests
describe('Structure', () => {
it('should render the decorative lines', () => {
// Arrange & Act
const { container } = render(<Empty onClearFilter={vi.fn()} />)
// Assert - there should be 4 Line components (SVG elements)
const svgElements = container.querySelectorAll('svg')
expect(svgElements.length).toBeGreaterThanOrEqual(4)
})
it('should render mask overlay', () => {
// Arrange & Act
const { container } = render(<Empty onClearFilter={vi.fn()} />)
// Assert
const maskElement = container.querySelector('.bg-dataset-chunk-list-mask-bg')
expect(maskElement).toBeInTheDocument()
})
it('should render icon container with proper styling', () => {
// Arrange & Act
const { container } = render(<Empty onClearFilter={vi.fn()} />)
// Assert
const iconContainer = container.querySelector('.shadow-lg')
expect(iconContainer).toBeInTheDocument()
})
it('should render clear filter button with accent text styling', () => {
// Arrange & Act
render(<Empty onClearFilter={vi.fn()} />)
// Assert
const button = screen.getByRole('button')
expect(button).toHaveClass('text-text-accent')
})
})
// Props tests
describe('Props', () => {
it('should accept onClearFilter callback prop', () => {
// Arrange
const mockCallback = vi.fn()
// Act
render(<Empty onClearFilter={mockCallback} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockCallback).toHaveBeenCalled()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle multiple clicks on clear filter button', () => {
// Arrange
const mockOnClearFilter = vi.fn()
render(<Empty onClearFilter={mockOnClearFilter} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.click(button)
// Assert
expect(mockOnClearFilter).toHaveBeenCalledTimes(3)
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(<Empty onClearFilter={vi.fn()} />)
// Act
rerender(<Empty onClearFilter={vi.fn()} />)
// Assert
const emptyCards = container.querySelectorAll('.bg-background-section-burn')
expect(emptyCards).toHaveLength(10)
})
})
})

View File

@ -0,0 +1,261 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import FullScreenDrawer from './full-screen-drawer'
// Mock the Drawer component since it has high complexity
vi.mock('./drawer', () => ({
default: ({ children, open, panelClassName, panelContentClassName, showOverlay, needCheckChunks, modal }: { children: React.ReactNode, open: boolean, panelClassName: string, panelContentClassName: string, showOverlay: boolean, needCheckChunks: boolean, modal: boolean }) => {
if (!open)
return null
return (
<div
data-testid="drawer-mock"
data-panel-class={panelClassName}
data-panel-content-class={panelContentClassName}
data-show-overlay={showOverlay}
data-need-check-chunks={needCheckChunks}
data-modal={modal}
>
{children}
</div>
)
},
}))
describe('FullScreenDrawer', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing when open', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
expect(screen.getByTestId('drawer-mock')).toBeInTheDocument()
})
it('should not render when closed', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={false} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
expect(screen.queryByTestId('drawer-mock')).not.toBeInTheDocument()
})
it('should render children content', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Test Content</div>
</FullScreenDrawer>,
)
// Assert
expect(screen.getByText('Test Content')).toBeInTheDocument()
})
})
// Props tests
describe('Props', () => {
it('should pass fullScreen=true to Drawer with full width class', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={true}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-panel-class')).toContain('w-full')
})
it('should pass fullScreen=false to Drawer with fixed width class', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-panel-class')).toContain('w-[568px]')
})
it('should pass showOverlay prop with default true', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-show-overlay')).toBe('true')
})
it('should pass showOverlay=false when specified', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false} showOverlay={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-show-overlay')).toBe('false')
})
it('should pass needCheckChunks prop with default false', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-need-check-chunks')).toBe('false')
})
it('should pass needCheckChunks=true when specified', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false} needCheckChunks={true}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-need-check-chunks')).toBe('true')
})
it('should pass modal prop with default false', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-modal')).toBe('false')
})
it('should pass modal=true when specified', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false} modal={true}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
expect(drawer.getAttribute('data-modal')).toBe('true')
})
})
// Styling tests
describe('Styling', () => {
it('should apply panel content classes for non-fullScreen mode', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
const contentClass = drawer.getAttribute('data-panel-content-class')
expect(contentClass).toContain('bg-components-panel-bg')
expect(contentClass).toContain('rounded-xl')
})
it('should apply panel content classes without border for fullScreen mode', () => {
// Arrange & Act
render(
<FullScreenDrawer isOpen={true} fullScreen={true}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
const drawer = screen.getByTestId('drawer-mock')
const contentClass = drawer.getAttribute('data-panel-content-class')
expect(contentClass).toContain('bg-components-panel-bg')
expect(contentClass).not.toContain('rounded-xl')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle undefined onClose gracefully', () => {
// Arrange & Act & Assert - should not throw
expect(() => {
render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
}).not.toThrow()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Act
rerender(
<FullScreenDrawer isOpen={true} fullScreen={true}>
<div>Updated Content</div>
</FullScreenDrawer>,
)
// Assert
expect(screen.getByText('Updated Content')).toBeInTheDocument()
})
it('should handle toggle between open and closed states', () => {
// Arrange
const { rerender } = render(
<FullScreenDrawer isOpen={true} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
expect(screen.getByTestId('drawer-mock')).toBeInTheDocument()
// Act
rerender(
<FullScreenDrawer isOpen={false} fullScreen={false}>
<div>Content</div>
</FullScreenDrawer>,
)
// Assert
expect(screen.queryByTestId('drawer-mock')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,249 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Keywords from './keywords'
describe('Keywords', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the keywords label', () => {
// Arrange & Act
render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert - i18n key format
expect(screen.getByText(/segment\.keywords/i)).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex')
expect(wrapper).toHaveClass('flex-col')
})
})
// Props tests
describe('Props', () => {
it('should display dash when no keywords and actionType is view', () => {
// Arrange & Act
render(
<Keywords
segInfo={{ id: '1', keywords: [] }}
keywords={[]}
onKeywordsChange={vi.fn()}
actionType="view"
/>,
)
// Assert
expect(screen.getByText('-')).toBeInTheDocument()
})
it('should not display dash when actionType is edit', () => {
// Arrange & Act
render(
<Keywords
segInfo={{ id: '1', keywords: [] }}
keywords={[]}
onKeywordsChange={vi.fn()}
actionType="edit"
/>,
)
// Assert
expect(screen.queryByText('-')).not.toBeInTheDocument()
})
it('should not display dash when actionType is add', () => {
// Arrange & Act
render(
<Keywords
segInfo={{ id: '1', keywords: [] }}
keywords={[]}
onKeywordsChange={vi.fn()}
actionType="add"
/>,
)
// Assert
expect(screen.queryByText('-')).not.toBeInTheDocument()
})
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
className="custom-class"
/>,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
})
it('should use default actionType of view', () => {
// Arrange & Act
render(
<Keywords
segInfo={{ id: '1', keywords: [] }}
keywords={[]}
onKeywordsChange={vi.fn()}
/>,
)
// Assert - dash should appear in view mode with empty keywords
expect(screen.getByText('-')).toBeInTheDocument()
})
})
// Structure tests
describe('Structure', () => {
it('should render label with uppercase styling', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert
const labelElement = container.querySelector('.system-xs-medium-uppercase')
expect(labelElement).toBeInTheDocument()
})
it('should render keywords container with overflow handling', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert
const keywordsContainer = container.querySelector('.overflow-auto')
expect(keywordsContainer).toBeInTheDocument()
})
it('should render keywords container with max height', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert
const keywordsContainer = container.querySelector('.max-h-\\[200px\\]')
expect(keywordsContainer).toBeInTheDocument()
})
})
// Edit mode tests
describe('Edit Mode', () => {
it('should render TagInput component when keywords exist', () => {
// Arrange & Act
const { container } = render(
<Keywords
segInfo={{ id: '1', keywords: ['keyword1', 'keyword2'] }}
keywords={['keyword1', 'keyword2']}
onKeywordsChange={vi.fn()}
isEditMode={true}
/>,
)
// Assert - TagInput should be rendered instead of dash
expect(screen.queryByText('-')).not.toBeInTheDocument()
expect(container.querySelector('.flex-wrap')).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty keywords array in view mode without segInfo keywords', () => {
// Arrange & Act
const { container } = render(
<Keywords
keywords={[]}
onKeywordsChange={vi.fn()}
actionType="view"
/>,
)
// Assert - container should be rendered
expect(container.firstChild).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(
<Keywords
segInfo={{ id: '1', keywords: ['test'] }}
keywords={['test']}
onKeywordsChange={vi.fn()}
/>,
)
// Act
rerender(
<Keywords
segInfo={{ id: '1', keywords: ['test', 'new'] }}
keywords={['test', 'new']}
onKeywordsChange={vi.fn()}
/>,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle segInfo with undefined keywords showing dash in view mode', () => {
// Arrange & Act
render(
<Keywords
segInfo={{ id: '1' }}
keywords={['test']}
onKeywordsChange={vi.fn()}
actionType="view"
/>,
)
// Assert - dash should show because segInfo.keywords is undefined/empty
expect(screen.getByText('-')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,150 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import RegenerationModal from './regeneration-modal'
// Create a wrapper component with event emitter context
const createWrapper = () => {
return ({ children }: { children: React.ReactNode }) => (
<EventEmitterContextProvider>
{children}
</EventEmitterContextProvider>
)
}
describe('RegenerationModal', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const defaultProps = {
isShow: true,
onConfirm: vi.fn(),
onCancel: vi.fn(),
onClose: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing when isShow is true', () => {
// Arrange & Act
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
// Assert
expect(screen.getByText(/segment\.regenerationConfirmTitle/i)).toBeInTheDocument()
})
it('should not render content when isShow is false', () => {
// Arrange & Act
render(<RegenerationModal {...defaultProps} isShow={false} />, { wrapper: createWrapper() })
// Assert - Modal container might exist but content should not be visible
expect(screen.queryByText(/segment\.regenerationConfirmTitle/i)).not.toBeInTheDocument()
})
it('should render confirmation message', () => {
// Arrange & Act
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
// Assert
expect(screen.getByText(/segment\.regenerationConfirmMessage/i)).toBeInTheDocument()
})
it('should render cancel button in default state', () => {
// Arrange & Act
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
// Assert
expect(screen.getByText(/operation\.cancel/i)).toBeInTheDocument()
})
it('should render regenerate button in default state', () => {
// Arrange & Act
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
// Assert
expect(screen.getByText(/operation\.regenerate/i)).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCancel when cancel button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
render(<RegenerationModal {...defaultProps} onCancel={mockOnCancel} />, { wrapper: createWrapper() })
// Act
fireEvent.click(screen.getByText(/operation\.cancel/i))
// Assert
expect(mockOnCancel).toHaveBeenCalledTimes(1)
})
it('should call onConfirm when regenerate button is clicked', () => {
// Arrange
const mockOnConfirm = vi.fn()
render(<RegenerationModal {...defaultProps} onConfirm={mockOnConfirm} />, { wrapper: createWrapper() })
// Act
fireEvent.click(screen.getByText(/operation\.regenerate/i))
// Assert
expect(mockOnConfirm).toHaveBeenCalledTimes(1)
})
})
// Modal content states - these would require event emitter manipulation
describe('Modal States', () => {
it('should show default content initially', () => {
// Arrange & Act
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
// Assert
expect(screen.getByText(/segment\.regenerationConfirmTitle/i)).toBeInTheDocument()
expect(screen.getByText(/operation\.cancel/i)).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle toggling isShow prop', () => {
// Arrange
const { rerender } = render(
<RegenerationModal {...defaultProps} isShow={true} />,
{ wrapper: createWrapper() },
)
expect(screen.getByText(/segment\.regenerationConfirmTitle/i)).toBeInTheDocument()
// Act
rerender(
<EventEmitterContextProvider>
<RegenerationModal {...defaultProps} isShow={false} />
</EventEmitterContextProvider>,
)
// Assert
expect(screen.queryByText(/segment\.regenerationConfirmTitle/i)).not.toBeInTheDocument()
})
it('should maintain handlers when rerendered', () => {
// Arrange
const mockOnConfirm = vi.fn()
const { rerender } = render(
<RegenerationModal {...defaultProps} onConfirm={mockOnConfirm} />,
{ wrapper: createWrapper() },
)
// Act
rerender(
<EventEmitterContextProvider>
<RegenerationModal {...defaultProps} onConfirm={mockOnConfirm} />
</EventEmitterContextProvider>,
)
fireEvent.click(screen.getByText(/operation\.regenerate/i))
// Assert
expect(mockOnConfirm).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -0,0 +1,215 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import SegmentIndexTag from './segment-index-tag'
describe('SegmentIndexTag', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<SegmentIndexTag positionId={1} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the Chunk icon', () => {
// Arrange & Act
const { container } = render(<SegmentIndexTag positionId={1} />)
// Assert
const icon = container.querySelector('.h-3.w-3')
expect(icon).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
const { container } = render(<SegmentIndexTag positionId={1} />)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex')
expect(wrapper).toHaveClass('items-center')
})
})
// Props tests
describe('Props', () => {
it('should render position ID with default prefix', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={5} />)
// Assert - default prefix is 'Chunk'
expect(screen.getByText('Chunk-05')).toBeInTheDocument()
})
it('should render position ID without padding for two-digit numbers', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={15} />)
// Assert
expect(screen.getByText('Chunk-15')).toBeInTheDocument()
})
it('should render position ID without padding for three-digit numbers', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={123} />)
// Assert
expect(screen.getByText('Chunk-123')).toBeInTheDocument()
})
it('should render custom label when provided', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={1} label="Custom Label" />)
// Assert
expect(screen.getByText('Custom Label')).toBeInTheDocument()
})
it('should use custom labelPrefix', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={3} labelPrefix="Segment" />)
// Assert
expect(screen.getByText('Segment-03')).toBeInTheDocument()
})
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<SegmentIndexTag positionId={1} className="custom-class" />,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
})
it('should apply custom iconClassName', () => {
// Arrange & Act
const { container } = render(
<SegmentIndexTag positionId={1} iconClassName="custom-icon-class" />,
)
// Assert
const icon = container.querySelector('.custom-icon-class')
expect(icon).toBeInTheDocument()
})
it('should apply custom labelClassName', () => {
// Arrange & Act
const { container } = render(
<SegmentIndexTag positionId={1} labelClassName="custom-label-class" />,
)
// Assert
const label = container.querySelector('.custom-label-class')
expect(label).toBeInTheDocument()
})
it('should handle string positionId', () => {
// Arrange & Act
render(<SegmentIndexTag positionId="7" />)
// Assert
expect(screen.getByText('Chunk-07')).toBeInTheDocument()
})
})
// Memoization tests
describe('Memoization', () => {
it('should compute localPositionId based on positionId and labelPrefix', () => {
// Arrange & Act
const { rerender } = render(<SegmentIndexTag positionId={1} />)
expect(screen.getByText('Chunk-01')).toBeInTheDocument()
// Act - change positionId
rerender(<SegmentIndexTag positionId={2} />)
// Assert
expect(screen.getByText('Chunk-02')).toBeInTheDocument()
})
it('should update when labelPrefix changes', () => {
// Arrange & Act
const { rerender } = render(<SegmentIndexTag positionId={1} labelPrefix="Chunk" />)
expect(screen.getByText('Chunk-01')).toBeInTheDocument()
// Act - change labelPrefix
rerender(<SegmentIndexTag positionId={1} labelPrefix="Part" />)
// Assert
expect(screen.getByText('Part-01')).toBeInTheDocument()
})
})
// Structure tests
describe('Structure', () => {
it('should render icon with tertiary text color', () => {
// Arrange & Act
const { container } = render(<SegmentIndexTag positionId={1} />)
// Assert
const icon = container.querySelector('.text-text-tertiary')
expect(icon).toBeInTheDocument()
})
it('should render label with xs medium font styling', () => {
// Arrange & Act
const { container } = render(<SegmentIndexTag positionId={1} />)
// Assert
const label = container.querySelector('.system-xs-medium')
expect(label).toBeInTheDocument()
})
it('should render icon with margin-right spacing', () => {
// Arrange & Act
const { container } = render(<SegmentIndexTag positionId={1} />)
// Assert
const icon = container.querySelector('.mr-0\\.5')
expect(icon).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle positionId of 0', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={0} />)
// Assert
expect(screen.getByText('Chunk-00')).toBeInTheDocument()
})
it('should handle undefined positionId', () => {
// Arrange & Act
render(<SegmentIndexTag />)
// Assert - should display 'Chunk-undefined' or similar
expect(screen.getByText(/Chunk-/)).toBeInTheDocument()
})
it('should prioritize label over computed positionId', () => {
// Arrange & Act
render(<SegmentIndexTag positionId={99} label="Override" />)
// Assert
expect(screen.getByText('Override')).toBeInTheDocument()
expect(screen.queryByText('Chunk-99')).not.toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(<SegmentIndexTag positionId={1} />)
// Act
rerender(<SegmentIndexTag positionId={1} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,151 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Tag from './tag'
describe('Tag', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<Tag text="test" />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the hash symbol', () => {
// Arrange & Act
render(<Tag text="test" />)
// Assert
expect(screen.getByText('#')).toBeInTheDocument()
})
it('should render the text content', () => {
// Arrange & Act
render(<Tag text="keyword" />)
// Assert
expect(screen.getByText('keyword')).toBeInTheDocument()
})
it('should render with correct base styling classes', () => {
// Arrange & Act
const { container } = render(<Tag text="test" />)
// Assert
const tagElement = container.firstChild as HTMLElement
expect(tagElement).toHaveClass('inline-flex')
expect(tagElement).toHaveClass('items-center')
expect(tagElement).toHaveClass('gap-x-0.5')
})
})
// Props tests
describe('Props', () => {
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(<Tag text="test" className="custom-class" />)
// Assert
const tagElement = container.firstChild as HTMLElement
expect(tagElement).toHaveClass('custom-class')
})
it('should render different text values', () => {
// Arrange & Act
const { rerender } = render(<Tag text="first" />)
expect(screen.getByText('first')).toBeInTheDocument()
// Act
rerender(<Tag text="second" />)
// Assert
expect(screen.getByText('second')).toBeInTheDocument()
})
})
// Structure tests
describe('Structure', () => {
it('should render hash with quaternary text color', () => {
// Arrange & Act
const { container } = render(<Tag text="test" />)
// Assert
const hashSpan = container.querySelector('.text-text-quaternary')
expect(hashSpan).toBeInTheDocument()
expect(hashSpan).toHaveTextContent('#')
})
it('should render text with tertiary text color', () => {
// Arrange & Act
const { container } = render(<Tag text="test" />)
// Assert
const textSpan = container.querySelector('.text-text-tertiary')
expect(textSpan).toBeInTheDocument()
expect(textSpan).toHaveTextContent('test')
})
it('should have truncate class for text overflow', () => {
// Arrange & Act
const { container } = render(<Tag text="very-long-text-that-might-overflow" />)
// Assert
const textSpan = container.querySelector('.truncate')
expect(textSpan).toBeInTheDocument()
})
it('should have max-width constraint on text', () => {
// Arrange & Act
const { container } = render(<Tag text="test" />)
// Assert
const textSpan = container.querySelector('.max-w-12')
expect(textSpan).toBeInTheDocument()
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently with same props', () => {
// Arrange & Act
const { container: container1 } = render(<Tag text="test" />)
const { container: container2 } = render(<Tag text="test" />)
// Assert
expect(container1.firstChild?.textContent).toBe(container2.firstChild?.textContent)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty text', () => {
// Arrange & Act
render(<Tag text="" />)
// Assert - should still render the hash symbol
expect(screen.getByText('#')).toBeInTheDocument()
})
it('should handle special characters in text', () => {
// Arrange & Act
render(<Tag text="test-tag_1" />)
// Assert
expect(screen.getByText('test-tag_1')).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<Tag text="test" />)
// Act
rerender(<Tag text="test" />)
// Assert
expect(screen.getByText('#')).toBeInTheDocument()
expect(screen.getByText('test')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,130 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import DisplayToggle from './display-toggle'
describe('DisplayToggle', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<DisplayToggle isCollapsed={true} toggleCollapsed={vi.fn()} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render button with proper styling', () => {
// Arrange & Act
render(<DisplayToggle isCollapsed={true} toggleCollapsed={vi.fn()} />)
// Assert
const button = screen.getByRole('button')
expect(button).toHaveClass('flex')
expect(button).toHaveClass('items-center')
expect(button).toHaveClass('justify-center')
expect(button).toHaveClass('rounded-lg')
})
})
// Props tests
describe('Props', () => {
it('should render expand icon when isCollapsed is true', () => {
// Arrange & Act
const { container } = render(
<DisplayToggle isCollapsed={true} toggleCollapsed={vi.fn()} />,
)
// Assert - RiLineHeight icon for expand
const icon = container.querySelector('.h-4.w-4')
expect(icon).toBeInTheDocument()
})
it('should render collapse icon when isCollapsed is false', () => {
// Arrange & Act
const { container } = render(
<DisplayToggle isCollapsed={false} toggleCollapsed={vi.fn()} />,
)
// Assert - Collapse icon
const icon = container.querySelector('.h-4.w-4')
expect(icon).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call toggleCollapsed when button is clicked', () => {
// Arrange
const mockToggle = vi.fn()
render(<DisplayToggle isCollapsed={true} toggleCollapsed={mockToggle} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockToggle).toHaveBeenCalledTimes(1)
})
it('should call toggleCollapsed on multiple clicks', () => {
// Arrange
const mockToggle = vi.fn()
render(<DisplayToggle isCollapsed={true} toggleCollapsed={mockToggle} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.click(button)
// Assert
expect(mockToggle).toHaveBeenCalledTimes(3)
})
})
// Tooltip tests
describe('Tooltip', () => {
it('should render with tooltip wrapper', () => {
// Arrange & Act
const { container } = render(
<DisplayToggle isCollapsed={true} toggleCollapsed={vi.fn()} />,
)
// Assert - Tooltip renders a wrapper around button
expect(container.firstChild).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should toggle icon when isCollapsed prop changes', () => {
// Arrange
const { rerender, container } = render(
<DisplayToggle isCollapsed={true} toggleCollapsed={vi.fn()} />,
)
// Act
rerender(<DisplayToggle isCollapsed={false} toggleCollapsed={vi.fn()} />)
// Assert - icon should still be present
const icon = container.querySelector('.h-4.w-4')
expect(icon).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(
<DisplayToggle isCollapsed={true} toggleCollapsed={vi.fn()} />,
)
// Act
rerender(<DisplayToggle isCollapsed={false} toggleCollapsed={vi.fn()} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,377 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import NewChildSegmentModal from './new-child-segment'
// Mock next/navigation
vi.mock('next/navigation', () => ({
useParams: () => ({
datasetId: 'test-dataset-id',
documentId: 'test-document-id',
}),
}))
// Mock ToastContext
const mockNotify = vi.fn()
vi.mock('use-context-selector', async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>
return {
...actual,
useContext: () => ({ notify: mockNotify }),
}
})
// Mock document context
let mockParentMode = 'paragraph'
vi.mock('../context', () => ({
useDocumentContext: (selector: (state: { parentMode: string }) => unknown) => {
return selector({ parentMode: mockParentMode })
},
}))
// Mock segment list context
let mockFullScreen = false
const mockToggleFullScreen = vi.fn()
vi.mock('./index', () => ({
useSegmentListContext: (selector: (state: { fullScreen: boolean, toggleFullScreen: () => void }) => unknown) => {
const state = {
fullScreen: mockFullScreen,
toggleFullScreen: mockToggleFullScreen,
}
return selector(state)
},
}))
// Mock useAddChildSegment
const mockAddChildSegment = vi.fn()
vi.mock('@/service/knowledge/use-segment', () => ({
useAddChildSegment: () => ({
mutateAsync: mockAddChildSegment,
}),
}))
// Mock app store
vi.mock('@/app/components/app/store', () => ({
useStore: () => ({ appSidebarExpand: 'expand' }),
}))
// Mock child components
vi.mock('./common/action-buttons', () => ({
default: ({ handleCancel, handleSave, loading, actionType, isChildChunk }: { handleCancel: () => void, handleSave: () => void, loading: boolean, actionType: string, isChildChunk?: boolean }) => (
<div data-testid="action-buttons">
<button onClick={handleCancel} data-testid="cancel-btn">Cancel</button>
<button onClick={handleSave} disabled={loading} data-testid="save-btn">
{loading ? 'Saving...' : 'Save'}
</button>
<span data-testid="action-type">{actionType}</span>
<span data-testid="is-child-chunk">{isChildChunk ? 'true' : 'false'}</span>
</div>
),
}))
vi.mock('./common/add-another', () => ({
default: ({ isChecked, onCheck, className }: { isChecked: boolean, onCheck: () => void, className?: string }) => (
<div data-testid="add-another" className={className}>
<input
type="checkbox"
checked={isChecked}
onChange={onCheck}
data-testid="add-another-checkbox"
/>
</div>
),
}))
vi.mock('./common/chunk-content', () => ({
default: ({ question, onQuestionChange, isEditMode }: { question: string, onQuestionChange: (v: string) => void, isEditMode: boolean }) => (
<div data-testid="chunk-content">
<input
data-testid="content-input"
value={question}
onChange={e => onQuestionChange(e.target.value)}
/>
<span data-testid="edit-mode">{isEditMode ? 'editing' : 'viewing'}</span>
</div>
),
}))
vi.mock('./common/dot', () => ({
default: () => <span data-testid="dot"></span>,
}))
vi.mock('./common/segment-index-tag', () => ({
SegmentIndexTag: ({ label }: { label: string }) => <span data-testid="segment-index-tag">{label}</span>,
}))
describe('NewChildSegmentModal', () => {
beforeEach(() => {
vi.clearAllMocks()
mockFullScreen = false
mockParentMode = 'paragraph'
})
const defaultProps = {
chunkId: 'chunk-1',
onCancel: vi.fn(),
onSave: vi.fn(),
viewNewlyAddedChildChunk: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render add child chunk title', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByText(/segment\.addChildChunk/i)).toBeInTheDocument()
})
it('should render chunk content component', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('chunk-content')).toBeInTheDocument()
})
it('should render segment index tag with new child chunk label', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('segment-index-tag')).toBeInTheDocument()
})
it('should render add another checkbox', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('add-another')).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCancel when close button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
const { container } = render(
<NewChildSegmentModal {...defaultProps} onCancel={mockOnCancel} />,
)
// Act
const closeButtons = container.querySelectorAll('.cursor-pointer')
if (closeButtons.length > 1)
fireEvent.click(closeButtons[1])
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
it('should call toggleFullScreen when expand button is clicked', () => {
// Arrange
const { container } = render(<NewChildSegmentModal {...defaultProps} />)
// Act
const expandButtons = container.querySelectorAll('.cursor-pointer')
if (expandButtons.length > 0)
fireEvent.click(expandButtons[0])
// Assert
expect(mockToggleFullScreen).toHaveBeenCalled()
})
it('should update content when input changes', () => {
// Arrange
render(<NewChildSegmentModal {...defaultProps} />)
// Act
fireEvent.change(screen.getByTestId('content-input'), {
target: { value: 'New content' },
})
// Assert
expect(screen.getByTestId('content-input')).toHaveValue('New content')
})
it('should toggle add another checkbox', () => {
// Arrange
render(<NewChildSegmentModal {...defaultProps} />)
const checkbox = screen.getByTestId('add-another-checkbox')
// Act
fireEvent.click(checkbox)
// Assert
expect(checkbox).toBeInTheDocument()
})
})
// Save validation
describe('Save Validation', () => {
it('should show error when content is empty', async () => {
// Arrange
render(<NewChildSegmentModal {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
})
})
// Successful save
describe('Successful Save', () => {
it('should call addChildSegment when valid content is provided', async () => {
// Arrange
mockAddChildSegment.mockImplementation((_params, options) => {
options.onSuccess({ data: { id: 'new-child-id' } })
options.onSettled()
return Promise.resolve()
})
render(<NewChildSegmentModal {...defaultProps} />)
fireEvent.change(screen.getByTestId('content-input'), {
target: { value: 'Valid content' },
})
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockAddChildSegment).toHaveBeenCalledWith(
expect.objectContaining({
datasetId: 'test-dataset-id',
documentId: 'test-document-id',
segmentId: 'chunk-1',
body: expect.objectContaining({
content: 'Valid content',
}),
}),
expect.any(Object),
)
})
})
it('should show success notification after save', async () => {
// Arrange
mockAddChildSegment.mockImplementation((_params, options) => {
options.onSuccess({ data: { id: 'new-child-id' } })
options.onSettled()
return Promise.resolve()
})
render(<NewChildSegmentModal {...defaultProps} />)
fireEvent.change(screen.getByTestId('content-input'), {
target: { value: 'Valid content' },
})
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'success',
}),
)
})
})
})
// Full screen mode
describe('Full Screen Mode', () => {
it('should show action buttons in header when fullScreen', () => {
// Arrange
mockFullScreen = true
// Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
it('should show add another in header when fullScreen', () => {
// Arrange
mockFullScreen = true
// Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('add-another')).toBeInTheDocument()
})
})
// Props
describe('Props', () => {
it('should pass actionType add to ActionButtons', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('action-type')).toHaveTextContent('add')
})
it('should pass isChildChunk true to ActionButtons', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('is-child-chunk')).toHaveTextContent('true')
})
it('should pass isEditMode true to ChunkContent', () => {
// Arrange & Act
render(<NewChildSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('edit-mode')).toHaveTextContent('editing')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle undefined viewNewlyAddedChildChunk', () => {
// Arrange
const props = { ...defaultProps, viewNewlyAddedChildChunk: undefined }
// Act
const { container } = render(<NewChildSegmentModal {...props} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<NewChildSegmentModal {...defaultProps} />)
// Act
rerender(<NewChildSegmentModal {...defaultProps} chunkId="chunk-2" />)
// Assert
expect(screen.getByTestId('chunk-content')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,269 @@
import { render, screen } from '@testing-library/react'
import { noop } from 'es-toolkit/function'
import { createContext, useContextSelector } from 'use-context-selector'
import { describe, expect, it, vi } from 'vitest'
import ChunkContent from './chunk-content'
// Create mock context matching the actual SegmentListContextValue
type SegmentListContextValue = {
isCollapsed: boolean
fullScreen: boolean
toggleFullScreen: (fullscreen?: boolean) => void
currSegment: { showModal: boolean }
currChildChunk: { showModal: boolean }
}
const MockSegmentListContext = createContext<SegmentListContextValue>({
isCollapsed: true,
fullScreen: false,
toggleFullScreen: noop,
currSegment: { showModal: false },
currChildChunk: { showModal: false },
})
// Mock the context module
vi.mock('..', () => ({
useSegmentListContext: (selector: (value: SegmentListContextValue) => unknown) => {
return useContextSelector(MockSegmentListContext, selector)
},
}))
// Helper to create wrapper with context
const createWrapper = (isCollapsed: boolean = true) => {
return ({ children }: { children: React.ReactNode }) => (
<MockSegmentListContext.Provider
value={{
isCollapsed,
fullScreen: false,
toggleFullScreen: noop,
currSegment: { showModal: false },
currChildChunk: { showModal: false },
}}
>
{children}
</MockSegmentListContext.Provider>
)
}
describe('ChunkContent', () => {
const defaultDetail = {
content: 'Test content',
sign_content: 'Test sign content',
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(
<ChunkContent detail={defaultDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render content in non-QA mode', () => {
// Arrange & Act
const { container } = render(
<ChunkContent detail={defaultDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert - should render without Q and A labels
expect(container.textContent).not.toContain('Q')
expect(container.textContent).not.toContain('A')
})
})
// QA mode tests
describe('QA Mode', () => {
it('should render Q and A labels when answer is present', () => {
// Arrange
const qaDetail = {
content: 'Question content',
sign_content: 'Sign content',
answer: 'Answer content',
}
// Act
render(
<ChunkContent detail={qaDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
})
it('should not render Q and A labels when answer is undefined', () => {
// Arrange & Act
render(
<ChunkContent detail={defaultDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert
expect(screen.queryByText('Q')).not.toBeInTheDocument()
expect(screen.queryByText('A')).not.toBeInTheDocument()
})
})
// Props tests
describe('Props', () => {
it('should apply custom className', () => {
// Arrange & Act
const { container } = render(
<ChunkContent
detail={defaultDetail}
isFullDocMode={false}
className="custom-class"
/>,
{ wrapper: createWrapper() },
)
// Assert
expect(container.querySelector('.custom-class')).toBeInTheDocument()
})
it('should handle isFullDocMode=true', () => {
// Arrange & Act
const { container } = render(
<ChunkContent detail={defaultDetail} isFullDocMode={true} />,
{ wrapper: createWrapper() },
)
// Assert - should have line-clamp-3 class
expect(container.querySelector('.line-clamp-3')).toBeInTheDocument()
})
it('should handle isFullDocMode=false with isCollapsed=true', () => {
// Arrange & Act
const { container } = render(
<ChunkContent detail={defaultDetail} isFullDocMode={false} />,
{ wrapper: createWrapper(true) },
)
// Assert - should have line-clamp-2 class
expect(container.querySelector('.line-clamp-2')).toBeInTheDocument()
})
it('should handle isFullDocMode=false with isCollapsed=false', () => {
// Arrange & Act
const { container } = render(
<ChunkContent detail={defaultDetail} isFullDocMode={false} />,
{ wrapper: createWrapper(false) },
)
// Assert - should have line-clamp-20 class
expect(container.querySelector('.line-clamp-20')).toBeInTheDocument()
})
})
// Content priority tests
describe('Content Priority', () => {
it('should prefer sign_content over content when both exist', () => {
// Arrange
const detail = {
content: 'Regular content',
sign_content: 'Sign content',
}
// Act
const { container } = render(
<ChunkContent detail={detail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert - The component uses sign_content || content
expect(container.firstChild).toBeInTheDocument()
})
it('should use content when sign_content is empty', () => {
// Arrange
const detail = {
content: 'Regular content',
sign_content: '',
}
// Act
const { container } = render(
<ChunkContent detail={detail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty content', () => {
// Arrange
const emptyDetail = {
content: '',
sign_content: '',
}
// Act
const { container } = render(
<ChunkContent detail={emptyDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle empty answer in QA mode', () => {
// Arrange
const qaDetail = {
content: 'Question',
sign_content: '',
answer: '',
}
// Act - empty answer is falsy, so QA mode won't render
render(
<ChunkContent detail={qaDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Assert - should not show Q and A labels since answer is empty string (falsy)
expect(screen.queryByText('Q')).not.toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(
<ChunkContent detail={defaultDetail} isFullDocMode={false} />,
{ wrapper: createWrapper() },
)
// Act
rerender(
<MockSegmentListContext.Provider
value={{
isCollapsed: true,
fullScreen: false,
toggleFullScreen: noop,
currSegment: { showModal: false },
currChildChunk: { showModal: false },
}}
>
<ChunkContent
detail={{ ...defaultDetail, content: 'Updated content' }}
isFullDocMode={false}
/>
</MockSegmentListContext.Provider>,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,507 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ChunkingMode } from '@/models/datasets'
import SegmentDetail from './segment-detail'
// Mock dataset detail context
let mockIndexingTechnique = IndexingType.QUALIFIED
let mockRuntimeMode = 'general'
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: (selector: (state: { dataset: { indexing_technique: string, runtime_mode: string } }) => unknown) => {
return selector({
dataset: {
indexing_technique: mockIndexingTechnique,
runtime_mode: mockRuntimeMode,
},
})
},
}))
// Mock document context
let mockParentMode = 'paragraph'
vi.mock('../context', () => ({
useDocumentContext: (selector: (state: { parentMode: string }) => unknown) => {
return selector({ parentMode: mockParentMode })
},
}))
// Mock segment list context
let mockFullScreen = false
const mockToggleFullScreen = vi.fn()
vi.mock('./index', () => ({
useSegmentListContext: (selector: (state: { fullScreen: boolean, toggleFullScreen: () => void }) => unknown) => {
const state = {
fullScreen: mockFullScreen,
toggleFullScreen: mockToggleFullScreen,
}
return selector(state)
},
}))
// Mock event emitter context
vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => ({
eventEmitter: {
useSubscription: vi.fn(),
},
}),
}))
// Mock child components
vi.mock('./common/action-buttons', () => ({
default: ({ handleCancel, handleSave, handleRegeneration, loading, showRegenerationButton }: { handleCancel: () => void, handleSave: () => void, handleRegeneration?: () => void, loading: boolean, showRegenerationButton?: boolean }) => (
<div data-testid="action-buttons">
<button onClick={handleCancel} data-testid="cancel-btn">Cancel</button>
<button onClick={handleSave} disabled={loading} data-testid="save-btn">Save</button>
{showRegenerationButton && (
<button onClick={handleRegeneration} data-testid="regenerate-btn">Regenerate</button>
)}
</div>
),
}))
vi.mock('./common/chunk-content', () => ({
default: ({ docForm, question, answer, onQuestionChange, onAnswerChange, isEditMode }: { docForm: string, question: string, answer: string, onQuestionChange: (v: string) => void, onAnswerChange: (v: string) => void, isEditMode: boolean }) => (
<div data-testid="chunk-content">
<input
data-testid="question-input"
value={question}
onChange={e => onQuestionChange(e.target.value)}
/>
{docForm === ChunkingMode.qa && (
<input
data-testid="answer-input"
value={answer}
onChange={e => onAnswerChange(e.target.value)}
/>
)}
<span data-testid="edit-mode">{isEditMode ? 'editing' : 'viewing'}</span>
</div>
),
}))
vi.mock('./common/dot', () => ({
default: () => <span data-testid="dot"></span>,
}))
vi.mock('./common/keywords', () => ({
default: ({ keywords, onKeywordsChange, _isEditMode, actionType }: { keywords: string[], onKeywordsChange: (v: string[]) => void, _isEditMode?: boolean, actionType: string }) => (
<div data-testid="keywords">
<span data-testid="keywords-action">{actionType}</span>
<input
data-testid="keywords-input"
value={keywords.join(',')}
onChange={e => onKeywordsChange(e.target.value.split(',').filter(Boolean))}
/>
</div>
),
}))
vi.mock('./common/segment-index-tag', () => ({
SegmentIndexTag: ({ positionId, label, labelPrefix }: { positionId?: string, label?: string, labelPrefix?: string }) => (
<span data-testid="segment-index-tag">
{labelPrefix}
{' '}
{positionId}
{' '}
{label}
</span>
),
}))
vi.mock('./common/regeneration-modal', () => ({
default: ({ isShow, onConfirm, onCancel, onClose }: { isShow: boolean, onConfirm: () => void, onCancel: () => void, onClose: () => void }) => (
isShow
? (
<div data-testid="regeneration-modal">
<button onClick={onConfirm} data-testid="confirm-regeneration">Confirm</button>
<button onClick={onCancel} data-testid="cancel-regeneration">Cancel</button>
<button onClick={onClose} data-testid="close-regeneration">Close</button>
</div>
)
: null
),
}))
vi.mock('@/app/components/datasets/common/image-uploader/image-uploader-in-chunk', () => ({
default: ({ disabled }: { value?: unknown[], onChange?: (v: unknown[]) => void, disabled?: boolean }) => (
<div data-testid="image-uploader">
<span data-testid="uploader-disabled">{disabled ? 'disabled' : 'enabled'}</span>
</div>
),
}))
describe('SegmentDetail', () => {
beforeEach(() => {
vi.clearAllMocks()
mockFullScreen = false
mockIndexingTechnique = IndexingType.QUALIFIED
mockRuntimeMode = 'general'
mockParentMode = 'paragraph'
})
const defaultSegInfo = {
id: 'segment-1',
content: 'Test content',
sign_content: 'Signed content',
answer: 'Test answer',
position: 1,
word_count: 100,
keywords: ['keyword1', 'keyword2'],
attachments: [],
}
const defaultProps = {
segInfo: defaultSegInfo,
onUpdate: vi.fn(),
onCancel: vi.fn(),
isEditMode: false,
docForm: ChunkingMode.text,
onModalStateChange: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<SegmentDetail {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render title for view mode', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={false} />)
// Assert
expect(screen.getByText(/segment\.chunkDetail/i)).toBeInTheDocument()
})
it('should render title for edit mode', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByText(/segment\.editChunk/i)).toBeInTheDocument()
})
it('should render chunk content component', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('chunk-content')).toBeInTheDocument()
})
it('should render image uploader', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('image-uploader')).toBeInTheDocument()
})
it('should render segment index tag', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('segment-index-tag')).toBeInTheDocument()
})
})
// Edit mode vs View mode
describe('Edit/View Mode', () => {
it('should pass isEditMode to ChunkContent', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('edit-mode')).toHaveTextContent('editing')
})
it('should disable image uploader in view mode', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={false} />)
// Assert
expect(screen.getByTestId('uploader-disabled')).toHaveTextContent('disabled')
})
it('should enable image uploader in edit mode', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('uploader-disabled')).toHaveTextContent('enabled')
})
it('should show action buttons in edit mode', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
it('should not show action buttons in view mode (non-fullscreen)', () => {
// Arrange & Act
render(<SegmentDetail {...defaultProps} isEditMode={false} />)
// Assert
expect(screen.queryByTestId('action-buttons')).not.toBeInTheDocument()
})
})
// Keywords display
describe('Keywords', () => {
it('should show keywords component when indexing is ECONOMICAL', () => {
// Arrange
mockIndexingTechnique = IndexingType.ECONOMICAL
// Act
render(<SegmentDetail {...defaultProps} />)
// Assert
expect(screen.getByTestId('keywords')).toBeInTheDocument()
})
it('should not show keywords when indexing is QUALIFIED', () => {
// Arrange
mockIndexingTechnique = IndexingType.QUALIFIED
// Act
render(<SegmentDetail {...defaultProps} />)
// Assert
expect(screen.queryByTestId('keywords')).not.toBeInTheDocument()
})
it('should pass view action type when not in edit mode', () => {
// Arrange
mockIndexingTechnique = IndexingType.ECONOMICAL
// Act
render(<SegmentDetail {...defaultProps} isEditMode={false} />)
// Assert
expect(screen.getByTestId('keywords-action')).toHaveTextContent('view')
})
it('should pass edit action type when in edit mode', () => {
// Arrange
mockIndexingTechnique = IndexingType.ECONOMICAL
// Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('keywords-action')).toHaveTextContent('edit')
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCancel when close button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
const { container } = render(<SegmentDetail {...defaultProps} onCancel={mockOnCancel} />)
// Act
const closeButtons = container.querySelectorAll('.cursor-pointer')
if (closeButtons.length > 1)
fireEvent.click(closeButtons[1])
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
it('should call toggleFullScreen when expand button is clicked', () => {
// Arrange
const { container } = render(<SegmentDetail {...defaultProps} />)
// Act
const expandButtons = container.querySelectorAll('.cursor-pointer')
if (expandButtons.length > 0)
fireEvent.click(expandButtons[0])
// Assert
expect(mockToggleFullScreen).toHaveBeenCalled()
})
it('should call onUpdate when save is clicked', () => {
// Arrange
const mockOnUpdate = vi.fn()
render(<SegmentDetail {...defaultProps} isEditMode={true} onUpdate={mockOnUpdate} />)
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
expect(mockOnUpdate).toHaveBeenCalledWith(
'segment-1',
expect.any(String),
expect.any(String),
expect.any(Array),
expect.any(Array),
)
})
it('should update question when input changes', () => {
// Arrange
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Act
fireEvent.change(screen.getByTestId('question-input'), {
target: { value: 'Updated content' },
})
// Assert
expect(screen.getByTestId('question-input')).toHaveValue('Updated content')
})
})
// Regeneration Modal
describe('Regeneration Modal', () => {
it('should show regeneration button when runtimeMode is general', () => {
// Arrange
mockRuntimeMode = 'general'
// Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('regenerate-btn')).toBeInTheDocument()
})
it('should not show regeneration button when runtimeMode is not general', () => {
// Arrange
mockRuntimeMode = 'pipeline'
// Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.queryByTestId('regenerate-btn')).not.toBeInTheDocument()
})
it('should show regeneration modal when regenerate is clicked', () => {
// Arrange
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Act
fireEvent.click(screen.getByTestId('regenerate-btn'))
// Assert
expect(screen.getByTestId('regeneration-modal')).toBeInTheDocument()
})
it('should call onModalStateChange when regeneration modal opens', () => {
// Arrange
const mockOnModalStateChange = vi.fn()
render(
<SegmentDetail
{...defaultProps}
isEditMode={true}
onModalStateChange={mockOnModalStateChange}
/>,
)
// Act
fireEvent.click(screen.getByTestId('regenerate-btn'))
// Assert
expect(mockOnModalStateChange).toHaveBeenCalledWith(true)
})
it('should close modal when cancel is clicked', () => {
// Arrange
const mockOnModalStateChange = vi.fn()
render(
<SegmentDetail
{...defaultProps}
isEditMode={true}
onModalStateChange={mockOnModalStateChange}
/>,
)
fireEvent.click(screen.getByTestId('regenerate-btn'))
// Act
fireEvent.click(screen.getByTestId('cancel-regeneration'))
// Assert
expect(mockOnModalStateChange).toHaveBeenCalledWith(false)
expect(screen.queryByTestId('regeneration-modal')).not.toBeInTheDocument()
})
})
// Full screen mode
describe('Full Screen Mode', () => {
it('should show action buttons in header when fullScreen and editMode', () => {
// Arrange
mockFullScreen = true
// Act
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
it('should apply full screen styling when fullScreen is true', () => {
// Arrange
mockFullScreen = true
// Act
const { container } = render(<SegmentDetail {...defaultProps} />)
// Assert
const header = container.querySelector('.border-divider-subtle')
expect(header).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle segInfo with minimal data', () => {
// Arrange
const minimalSegInfo = {
id: 'segment-minimal',
position: 1,
word_count: 0,
}
// Act
const { container } = render(<SegmentDetail {...defaultProps} segInfo={minimalSegInfo} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle empty keywords array', () => {
// Arrange
mockIndexingTechnique = IndexingType.ECONOMICAL
const segInfo = { ...defaultSegInfo, keywords: [] }
// Act
render(<SegmentDetail {...defaultProps} segInfo={segInfo} />)
// Assert
expect(screen.getByTestId('keywords-input')).toHaveValue('')
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<SegmentDetail {...defaultProps} isEditMode={false} />)
// Act
rerender(<SegmentDetail {...defaultProps} isEditMode={true} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,297 @@
import type { SegmentDetailModel } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import SegmentList from './segment-list'
// Mock document context
let mockDocForm = ChunkingMode.text
let mockParentMode = 'paragraph'
vi.mock('../context', () => ({
useDocumentContext: (selector: (state: { docForm: ChunkingMode, parentMode: string }) => unknown) => {
return selector({
docForm: mockDocForm,
parentMode: mockParentMode,
})
},
}))
// Mock segment list context
let mockCurrSegment: { segInfo: { id: string } } | null = null
let mockCurrChildChunk: { childChunkInfo: { segment_id: string } } | null = null
vi.mock('./index', () => ({
useSegmentListContext: (selector: (state: { currSegment: { segInfo: { id: string } } | null, currChildChunk: { childChunkInfo: { segment_id: string } } | null }) => unknown) => {
return selector({
currSegment: mockCurrSegment,
currChildChunk: mockCurrChildChunk,
})
},
}))
// Mock child components
vi.mock('./common/empty', () => ({
default: ({ onClearFilter }: { onClearFilter: () => void }) => (
<div data-testid="empty">
<button onClick={onClearFilter} data-testid="clear-filter-btn">Clear Filter</button>
</div>
),
}))
vi.mock('./segment-card', () => ({
default: ({ detail, onClick, _onChangeSwitch, archived, embeddingAvailable, focused }: { detail: SegmentDetailModel, onClick: () => void, _onChangeSwitch?: () => void, archived: boolean, embeddingAvailable: boolean, focused: { segmentIndex: boolean, segmentContent: boolean } }) => (
<div data-testid="segment-card" data-id={detail.id}>
<span data-testid="segment-content">{detail.content}</span>
<span data-testid="archived">{archived ? 'true' : 'false'}</span>
<span data-testid="embedding-available">{embeddingAvailable ? 'true' : 'false'}</span>
<span data-testid="focused-index">{focused.segmentIndex ? 'true' : 'false'}</span>
<span data-testid="focused-content">{focused.segmentContent ? 'true' : 'false'}</span>
<button onClick={onClick} data-testid="card-click">Click</button>
</div>
),
}))
vi.mock('./skeleton/general-list-skeleton', () => ({
default: () => <div data-testid="general-skeleton">Loading...</div>,
}))
vi.mock('./skeleton/paragraph-list-skeleton', () => ({
default: () => <div data-testid="paragraph-skeleton">Loading Paragraph...</div>,
}))
describe('SegmentList', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDocForm = ChunkingMode.text
mockParentMode = 'paragraph'
mockCurrSegment = null
mockCurrChildChunk = null
})
const createMockSegment = (id: string, content: string): SegmentDetailModel => ({
id,
content,
position: 1,
word_count: 10,
tokens: 5,
hit_count: 0,
enabled: true,
status: 'completed',
created_at: Date.now(),
updated_at: Date.now(),
keywords: [],
document_id: 'doc-1',
sign_content: content,
index_node_id: `index-${id}`,
index_node_hash: `hash-${id}`,
answer: '',
error: null,
disabled_at: null,
disabled_by: null,
} as unknown as SegmentDetailModel)
const defaultProps = {
ref: null,
isLoading: false,
items: [createMockSegment('seg-1', 'Segment 1 content')],
selectedSegmentIds: [],
onSelected: vi.fn(),
onClick: vi.fn(),
onChangeSwitch: vi.fn(),
onDelete: vi.fn(),
onDeleteChildChunk: vi.fn(),
handleAddNewChildChunk: vi.fn(),
onClickSlice: vi.fn(),
archived: false,
embeddingAvailable: true,
onClearFilter: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<SegmentList {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render segment cards for each item', () => {
// Arrange
const items = [
createMockSegment('seg-1', 'Content 1'),
createMockSegment('seg-2', 'Content 2'),
]
// Act
render(<SegmentList {...defaultProps} items={items} />)
// Assert
expect(screen.getAllByTestId('segment-card')).toHaveLength(2)
})
it('should render empty component when items is empty', () => {
// Arrange & Act
render(<SegmentList {...defaultProps} items={[]} />)
// Assert
expect(screen.getByTestId('empty')).toBeInTheDocument()
})
})
// Loading state
describe('Loading State', () => {
it('should render general skeleton when loading and docForm is text', () => {
// Arrange
mockDocForm = ChunkingMode.text
// Act
render(<SegmentList {...defaultProps} isLoading={true} />)
// Assert
expect(screen.getByTestId('general-skeleton')).toBeInTheDocument()
})
it('should render paragraph skeleton when loading and docForm is parentChild with paragraph mode', () => {
// Arrange
mockDocForm = ChunkingMode.parentChild
mockParentMode = 'paragraph'
// Act
render(<SegmentList {...defaultProps} isLoading={true} />)
// Assert
expect(screen.getByTestId('paragraph-skeleton')).toBeInTheDocument()
})
it('should render general skeleton when loading and docForm is parentChild with full-doc mode', () => {
// Arrange
mockDocForm = ChunkingMode.parentChild
mockParentMode = 'full-doc'
// Act
render(<SegmentList {...defaultProps} isLoading={true} />)
// Assert
expect(screen.getByTestId('general-skeleton')).toBeInTheDocument()
})
})
// Props passing
describe('Props Passing', () => {
it('should pass archived prop to SegmentCard', () => {
// Arrange & Act
render(<SegmentList {...defaultProps} archived={true} />)
// Assert
expect(screen.getByTestId('archived')).toHaveTextContent('true')
})
it('should pass embeddingAvailable prop to SegmentCard', () => {
// Arrange & Act
render(<SegmentList {...defaultProps} embeddingAvailable={false} />)
// Assert
expect(screen.getByTestId('embedding-available')).toHaveTextContent('false')
})
})
// Focused state
describe('Focused State', () => {
it('should set focused index when currSegment matches', () => {
// Arrange
mockCurrSegment = { segInfo: { id: 'seg-1' } }
// Act
render(<SegmentList {...defaultProps} />)
// Assert
expect(screen.getByTestId('focused-index')).toHaveTextContent('true')
})
it('should set focused content when currSegment matches', () => {
// Arrange
mockCurrSegment = { segInfo: { id: 'seg-1' } }
// Act
render(<SegmentList {...defaultProps} />)
// Assert
expect(screen.getByTestId('focused-content')).toHaveTextContent('true')
})
it('should set focused when currChildChunk parent matches', () => {
// Arrange
mockCurrChildChunk = { childChunkInfo: { segment_id: 'seg-1' } }
// Act
render(<SegmentList {...defaultProps} />)
// Assert
expect(screen.getByTestId('focused-index')).toHaveTextContent('true')
})
})
// Clear filter
describe('Clear Filter', () => {
it('should call onClearFilter when clear filter button is clicked', async () => {
// Arrange
const mockOnClearFilter = vi.fn()
render(<SegmentList {...defaultProps} items={[]} onClearFilter={mockOnClearFilter} />)
// Act
screen.getByTestId('clear-filter-btn').click()
// Assert
expect(mockOnClearFilter).toHaveBeenCalled()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle single item without divider', () => {
// Arrange & Act
render(<SegmentList {...defaultProps} items={[createMockSegment('seg-1', 'Content')]} />)
// Assert
expect(screen.getByTestId('segment-card')).toBeInTheDocument()
})
it('should handle multiple items with dividers', () => {
// Arrange
const items = [
createMockSegment('seg-1', 'Content 1'),
createMockSegment('seg-2', 'Content 2'),
createMockSegment('seg-3', 'Content 3'),
]
// Act
render(<SegmentList {...defaultProps} items={items} />)
// Assert
expect(screen.getAllByTestId('segment-card')).toHaveLength(3)
})
it('should maintain structure when rerendered with different items', () => {
// Arrange
const { rerender } = render(
<SegmentList {...defaultProps} items={[createMockSegment('seg-1', 'Content 1')]} />,
)
// Act
rerender(
<SegmentList
{...defaultProps}
items={[
createMockSegment('seg-2', 'Content 2'),
createMockSegment('seg-3', 'Content 3'),
]}
/>,
)
// Assert
expect(screen.getAllByTestId('segment-card')).toHaveLength(2)
})
})
})

View File

@ -0,0 +1,124 @@
import { render } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import FullDocListSkeleton from './full-doc-list-skeleton'
describe('FullDocListSkeleton', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the correct number of slice elements', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert - component renders 15 slices
const sliceElements = container.querySelectorAll('.flex.flex-col.gap-y-1')
expect(sliceElements).toHaveLength(15)
})
it('should render mask overlay element', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert - check for the mask overlay element
const maskElement = container.querySelector('.bg-dataset-chunk-list-mask-bg')
expect(maskElement).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert
const containerElement = container.firstChild as HTMLElement
expect(containerElement).toHaveClass('relative')
expect(containerElement).toHaveClass('z-10')
expect(containerElement).toHaveClass('flex')
expect(containerElement).toHaveClass('w-full')
expect(containerElement).toHaveClass('grow')
expect(containerElement).toHaveClass('flex-col')
expect(containerElement).toHaveClass('gap-y-3')
expect(containerElement).toHaveClass('overflow-y-hidden')
})
})
// Structure tests
describe('Structure', () => {
it('should render slice elements with proper structure', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert - each slice should have the content placeholder elements
const slices = container.querySelectorAll('.flex.flex-col.gap-y-1')
slices.forEach((slice) => {
// Each slice should have children for the skeleton content
expect(slice.children.length).toBeGreaterThan(0)
})
})
it('should render slice with width placeholder elements', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert - check for skeleton content width class
const widthElements = container.querySelectorAll('.w-2\\/3')
expect(widthElements.length).toBeGreaterThan(0)
})
it('should render slice elements with background classes', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert - check for skeleton background classes
const bgElements = container.querySelectorAll('.bg-state-base-hover')
expect(bgElements.length).toBeGreaterThan(0)
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently across multiple renders', () => {
// Arrange & Act
const { container: container1 } = render(<FullDocListSkeleton />)
const { container: container2 } = render(<FullDocListSkeleton />)
// Assert - structure should be identical
const slices1 = container1.querySelectorAll('.flex.flex-col.gap-y-1')
const slices2 = container2.querySelectorAll('.flex.flex-col.gap-y-1')
expect(slices1.length).toBe(slices2.length)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rendered multiple times', () => {
// Arrange
const { rerender, container } = render(<FullDocListSkeleton />)
// Act
rerender(<FullDocListSkeleton />)
rerender(<FullDocListSkeleton />)
// Assert
const sliceElements = container.querySelectorAll('.flex.flex-col.gap-y-1')
expect(sliceElements).toHaveLength(15)
})
it('should not have accessibility issues with skeleton content', () => {
// Arrange & Act
const { container } = render(<FullDocListSkeleton />)
// Assert - skeleton should be purely visual, no interactive elements
const buttons = container.querySelectorAll('button')
const links = container.querySelectorAll('a')
expect(buttons).toHaveLength(0)
expect(links).toHaveLength(0)
})
})
})

View File

@ -0,0 +1,195 @@
import { render } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import GeneralListSkeleton, { CardSkelton } from './general-list-skeleton'
describe('CardSkelton', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<CardSkelton />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render skeleton rows', () => {
// Arrange & Act
const { container } = render(<CardSkelton />)
// Assert - component should have skeleton rectangle elements
const skeletonRectangles = container.querySelectorAll('.bg-text-quaternary')
expect(skeletonRectangles.length).toBeGreaterThan(0)
})
it('should render with proper container padding', () => {
// Arrange & Act
const { container } = render(<CardSkelton />)
// Assert
expect(container.querySelector('.p-1')).toBeInTheDocument()
expect(container.querySelector('.pb-2')).toBeInTheDocument()
})
})
// Structure tests
describe('Structure', () => {
it('should render skeleton points as separators', () => {
// Arrange & Act
const { container } = render(<CardSkelton />)
// Assert - check for opacity class on skeleton points
const opacityElements = container.querySelectorAll('.opacity-20')
expect(opacityElements.length).toBeGreaterThan(0)
})
it('should render width-constrained skeleton elements', () => {
// Arrange & Act
const { container } = render(<CardSkelton />)
// Assert - check for various width classes
expect(container.querySelector('.w-\\[72px\\]')).toBeInTheDocument()
expect(container.querySelector('.w-24')).toBeInTheDocument()
expect(container.querySelector('.w-full')).toBeInTheDocument()
})
})
})
describe('GeneralListSkeleton', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the correct number of list items', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert - component renders 10 items (Checkbox is a div with shrink-0 and h-4 w-4)
const listItems = container.querySelectorAll('.items-start.gap-x-2')
expect(listItems).toHaveLength(10)
})
it('should render mask overlay element', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert
const maskElement = container.querySelector('.bg-dataset-chunk-list-mask-bg')
expect(maskElement).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert
const containerElement = container.firstChild as HTMLElement
expect(containerElement).toHaveClass('relative')
expect(containerElement).toHaveClass('z-10')
expect(containerElement).toHaveClass('flex')
expect(containerElement).toHaveClass('grow')
expect(containerElement).toHaveClass('flex-col')
expect(containerElement).toHaveClass('overflow-y-hidden')
})
})
// Checkbox tests
describe('Checkboxes', () => {
it('should render disabled checkboxes', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert - Checkbox component uses cursor-not-allowed class when disabled
const disabledCheckboxes = container.querySelectorAll('.cursor-not-allowed')
expect(disabledCheckboxes.length).toBeGreaterThan(0)
})
it('should render checkboxes with shrink-0 class for consistent sizing', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert
const checkboxContainers = container.querySelectorAll('.shrink-0')
expect(checkboxContainers.length).toBeGreaterThan(0)
})
})
// Divider tests
describe('Dividers', () => {
it('should render dividers between items except for the last one', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert - should have 9 dividers (not after last item)
const dividers = container.querySelectorAll('.bg-divider-subtle')
expect(dividers).toHaveLength(9)
})
})
// Structure tests
describe('Structure', () => {
it('should render list items with proper gap styling', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert
const listItems = container.querySelectorAll('.gap-x-2')
expect(listItems.length).toBeGreaterThan(0)
})
it('should render CardSkelton inside each list item', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert - each list item should contain card skeleton content
const cardContainers = container.querySelectorAll('.grow')
expect(cardContainers.length).toBeGreaterThan(0)
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently across multiple renders', () => {
// Arrange & Act
const { container: container1 } = render(<GeneralListSkeleton />)
const { container: container2 } = render(<GeneralListSkeleton />)
// Assert
const checkboxes1 = container1.querySelectorAll('input[type="checkbox"]')
const checkboxes2 = container2.querySelectorAll('input[type="checkbox"]')
expect(checkboxes1.length).toBe(checkboxes2.length)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(<GeneralListSkeleton />)
// Act
rerender(<GeneralListSkeleton />)
// Assert
const listItems = container.querySelectorAll('.items-start.gap-x-2')
expect(listItems).toHaveLength(10)
})
it('should not have interactive elements besides disabled checkboxes', () => {
// Arrange & Act
const { container } = render(<GeneralListSkeleton />)
// Assert
const buttons = container.querySelectorAll('button')
const links = container.querySelectorAll('a')
expect(buttons).toHaveLength(0)
expect(links).toHaveLength(0)
})
})
})

View File

@ -0,0 +1,151 @@
import { render } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import ParagraphListSkeleton from './paragraph-list-skeleton'
describe('ParagraphListSkeleton', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the correct number of list items', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert - component renders 10 items
const listItems = container.querySelectorAll('.items-start.gap-x-2')
expect(listItems).toHaveLength(10)
})
it('should render mask overlay element', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert
const maskElement = container.querySelector('.bg-dataset-chunk-list-mask-bg')
expect(maskElement).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert
const containerElement = container.firstChild as HTMLElement
expect(containerElement).toHaveClass('relative')
expect(containerElement).toHaveClass('z-10')
expect(containerElement).toHaveClass('flex')
expect(containerElement).toHaveClass('h-full')
expect(containerElement).toHaveClass('flex-col')
expect(containerElement).toHaveClass('overflow-y-hidden')
})
})
// Checkbox tests
describe('Checkboxes', () => {
it('should render disabled checkboxes', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert - Checkbox component uses cursor-not-allowed class when disabled
const disabledCheckboxes = container.querySelectorAll('.cursor-not-allowed')
expect(disabledCheckboxes.length).toBeGreaterThan(0)
})
it('should render checkboxes with shrink-0 class for consistent sizing', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert
const checkboxContainers = container.querySelectorAll('.shrink-0')
expect(checkboxContainers.length).toBeGreaterThan(0)
})
})
// Divider tests
describe('Dividers', () => {
it('should render dividers between items except for the last one', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert - should have 9 dividers (not after last item)
const dividers = container.querySelectorAll('.bg-divider-subtle')
expect(dividers).toHaveLength(9)
})
})
// Structure tests
describe('Structure', () => {
it('should render arrow icon for expand button styling', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert - paragraph list skeleton has expand button styled area
const expandBtnElements = container.querySelectorAll('.bg-dataset-child-chunk-expand-btn-bg')
expect(expandBtnElements.length).toBeGreaterThan(0)
})
it('should render skeleton rectangles with quaternary text color', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert
const skeletonElements = container.querySelectorAll('.bg-text-quaternary')
expect(skeletonElements.length).toBeGreaterThan(0)
})
it('should render CardSkelton inside each list item', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert - each list item should contain card skeleton content
const cardContainers = container.querySelectorAll('.grow')
expect(cardContainers.length).toBeGreaterThan(0)
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently across multiple renders', () => {
// Arrange & Act
const { container: container1 } = render(<ParagraphListSkeleton />)
const { container: container2 } = render(<ParagraphListSkeleton />)
// Assert
const items1 = container1.querySelectorAll('.items-start.gap-x-2')
const items2 = container2.querySelectorAll('.items-start.gap-x-2')
expect(items1.length).toBe(items2.length)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(<ParagraphListSkeleton />)
// Act
rerender(<ParagraphListSkeleton />)
// Assert
const listItems = container.querySelectorAll('.items-start.gap-x-2')
expect(listItems).toHaveLength(10)
})
it('should not have interactive elements besides disabled checkboxes', () => {
// Arrange & Act
const { container } = render(<ParagraphListSkeleton />)
// Assert
const buttons = container.querySelectorAll('button')
const links = container.querySelectorAll('a')
expect(buttons).toHaveLength(0)
expect(links).toHaveLength(0)
})
})
})

View File

@ -0,0 +1,132 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import ParentChunkCardSkelton from './parent-chunk-card-skeleton'
describe('ParentChunkCardSkelton', () => {
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<ParentChunkCardSkelton />)
// Assert
expect(screen.getByTestId('parent-chunk-card-skeleton')).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
render(<ParentChunkCardSkelton />)
// Assert
const container = screen.getByTestId('parent-chunk-card-skeleton')
expect(container).toHaveClass('flex')
expect(container).toHaveClass('flex-col')
expect(container).toHaveClass('pb-2')
})
it('should render skeleton rectangles', () => {
// Arrange & Act
const { container } = render(<ParentChunkCardSkelton />)
// Assert
const skeletonRectangles = container.querySelectorAll('.bg-text-quaternary')
expect(skeletonRectangles.length).toBeGreaterThan(0)
})
})
// i18n tests
describe('i18n', () => {
it('should render view more button with translated text', () => {
// Arrange & Act
render(<ParentChunkCardSkelton />)
// Assert - the button should contain translated text
const viewMoreButton = screen.getByRole('button')
expect(viewMoreButton).toBeInTheDocument()
})
it('should render disabled view more button', () => {
// Arrange & Act
render(<ParentChunkCardSkelton />)
// Assert
const viewMoreButton = screen.getByRole('button')
expect(viewMoreButton).toBeDisabled()
})
})
// Structure tests
describe('Structure', () => {
it('should render skeleton points as separators', () => {
// Arrange & Act
const { container } = render(<ParentChunkCardSkelton />)
// Assert
const opacityElements = container.querySelectorAll('.opacity-20')
expect(opacityElements.length).toBeGreaterThan(0)
})
it('should render width-constrained skeleton elements', () => {
// Arrange & Act
const { container } = render(<ParentChunkCardSkelton />)
// Assert - check for various width classes
expect(container.querySelector('.w-\\[72px\\]')).toBeInTheDocument()
expect(container.querySelector('.w-24')).toBeInTheDocument()
expect(container.querySelector('.w-full')).toBeInTheDocument()
expect(container.querySelector('.w-2\\/3')).toBeInTheDocument()
})
it('should render button with proper styling classes', () => {
// Arrange & Act
render(<ParentChunkCardSkelton />)
// Assert
const button = screen.getByRole('button')
expect(button).toHaveClass('system-xs-semibold-uppercase')
expect(button).toHaveClass('text-components-button-secondary-accent-text-disabled')
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently across multiple renders', () => {
// Arrange & Act
const { container: container1 } = render(<ParentChunkCardSkelton />)
const { container: container2 } = render(<ParentChunkCardSkelton />)
// Assert
const skeletons1 = container1.querySelectorAll('.bg-text-quaternary')
const skeletons2 = container2.querySelectorAll('.bg-text-quaternary')
expect(skeletons1.length).toBe(skeletons2.length)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, container } = render(<ParentChunkCardSkelton />)
// Act
rerender(<ParentChunkCardSkelton />)
// Assert
expect(screen.getByTestId('parent-chunk-card-skeleton')).toBeInTheDocument()
const skeletons = container.querySelectorAll('.bg-text-quaternary')
expect(skeletons.length).toBeGreaterThan(0)
})
it('should have only one interactive element (disabled button)', () => {
// Arrange & Act
const { container } = render(<ParentChunkCardSkelton />)
// Assert
const buttons = container.querySelectorAll('button')
const links = container.querySelectorAll('a')
expect(buttons).toHaveLength(1)
expect(buttons[0]).toBeDisabled()
expect(links).toHaveLength(0)
})
})
})

View File

@ -0,0 +1,118 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import StatusItem from './status-item'
describe('StatusItem', () => {
const defaultItem = {
value: '1',
name: 'Test Status',
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<StatusItem item={defaultItem} selected={false} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render item name', () => {
// Arrange & Act
render(<StatusItem item={defaultItem} selected={false} />)
// Assert
expect(screen.getByText('Test Status')).toBeInTheDocument()
})
it('should render with correct styling classes', () => {
// Arrange & Act
const { container } = render(<StatusItem item={defaultItem} selected={false} />)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex')
expect(wrapper).toHaveClass('items-center')
expect(wrapper).toHaveClass('justify-between')
})
})
// Props tests
describe('Props', () => {
it('should show check icon when selected is true', () => {
// Arrange & Act
const { container } = render(<StatusItem item={defaultItem} selected={true} />)
// Assert - RiCheckLine icon should be present
const checkIcon = container.querySelector('.text-text-accent')
expect(checkIcon).toBeInTheDocument()
})
it('should not show check icon when selected is false', () => {
// Arrange & Act
const { container } = render(<StatusItem item={defaultItem} selected={false} />)
// Assert - RiCheckLine icon should not be present
const checkIcon = container.querySelector('.text-text-accent')
expect(checkIcon).not.toBeInTheDocument()
})
it('should render different item names', () => {
// Arrange & Act
const item = { value: '2', name: 'Different Status' }
render(<StatusItem item={item} selected={false} />)
// Assert
expect(screen.getByText('Different Status')).toBeInTheDocument()
})
})
// Memoization tests
describe('Memoization', () => {
it('should render consistently with same props', () => {
// Arrange & Act
const { container: container1 } = render(<StatusItem item={defaultItem} selected={true} />)
const { container: container2 } = render(<StatusItem item={defaultItem} selected={true} />)
// Assert
expect(container1.textContent).toBe(container2.textContent)
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty item name', () => {
// Arrange
const emptyItem = { value: '1', name: '' }
// Act
const { container } = render(<StatusItem item={emptyItem} selected={false} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle special characters in item name', () => {
// Arrange
const specialItem = { value: '1', name: 'Status <>&"' }
// Act
render(<StatusItem item={specialItem} selected={false} />)
// Assert
expect(screen.getByText('Status <>&"')).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<StatusItem item={defaultItem} selected={false} />)
// Act
rerender(<StatusItem item={defaultItem} selected={true} />)
// Assert
expect(screen.getByText('Test Status')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,169 @@
import { render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { DocumentTitle } from './document-title'
// Mock next/navigation
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
}))
// Mock DocumentPicker
vi.mock('../../common/document-picker', () => ({
default: ({ datasetId, value, onChange }: { datasetId: string, value: unknown, onChange: (doc: { id: string }) => void }) => (
<div
data-testid="document-picker"
data-dataset-id={datasetId}
data-value={JSON.stringify(value)}
onClick={() => onChange({ id: 'new-doc-id' })}
>
Document Picker
</div>
),
}))
describe('DocumentTitle', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(
<DocumentTitle datasetId="dataset-1" />,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render DocumentPicker component', () => {
// Arrange & Act
const { getByTestId } = render(
<DocumentTitle datasetId="dataset-1" />,
)
// Assert
expect(getByTestId('document-picker')).toBeInTheDocument()
})
it('should render with correct container classes', () => {
// Arrange & Act
const { container } = render(
<DocumentTitle datasetId="dataset-1" />,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex')
expect(wrapper).toHaveClass('flex-1')
expect(wrapper).toHaveClass('items-center')
expect(wrapper).toHaveClass('justify-start')
})
})
// Props tests
describe('Props', () => {
it('should pass datasetId to DocumentPicker', () => {
// Arrange & Act
const { getByTestId } = render(
<DocumentTitle datasetId="test-dataset-id" />,
)
// Assert
expect(getByTestId('document-picker').getAttribute('data-dataset-id')).toBe('test-dataset-id')
})
it('should pass value props to DocumentPicker', () => {
// Arrange & Act
const { getByTestId } = render(
<DocumentTitle
datasetId="dataset-1"
name="test-document"
extension="pdf"
chunkingMode={ChunkingMode.text}
parent_mode="paragraph"
/>,
)
// Assert
const value = JSON.parse(getByTestId('document-picker').getAttribute('data-value') || '{}')
expect(value.name).toBe('test-document')
expect(value.extension).toBe('pdf')
expect(value.chunkingMode).toBe(ChunkingMode.text)
expect(value.parentMode).toBe('paragraph')
})
it('should default parentMode to paragraph when parent_mode is undefined', () => {
// Arrange & Act
const { getByTestId } = render(
<DocumentTitle datasetId="dataset-1" />,
)
// Assert
const value = JSON.parse(getByTestId('document-picker').getAttribute('data-value') || '{}')
expect(value.parentMode).toBe('paragraph')
})
it('should apply custom wrapperCls', () => {
// Arrange & Act
const { container } = render(
<DocumentTitle datasetId="dataset-1" wrapperCls="custom-wrapper" />,
)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-wrapper')
})
})
// Navigation tests
describe('Navigation', () => {
it('should navigate to document page when document is selected', () => {
// Arrange
const { getByTestId } = render(
<DocumentTitle datasetId="dataset-1" />,
)
// Act
getByTestId('document-picker').click()
// Assert
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents/new-doc-id')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle undefined optional props', () => {
// Arrange & Act
const { getByTestId } = render(
<DocumentTitle datasetId="dataset-1" />,
)
// Assert
const value = JSON.parse(getByTestId('document-picker').getAttribute('data-value') || '{}')
expect(value.name).toBeUndefined()
expect(value.extension).toBeUndefined()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender, getByTestId } = render(
<DocumentTitle datasetId="dataset-1" name="doc1" />,
)
// Act
rerender(<DocumentTitle datasetId="dataset-2" name="doc2" />)
// Assert
expect(getByTestId('document-picker').getAttribute('data-dataset-id')).toBe('dataset-2')
})
})
})

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { DataSourceInfo, FileItem, LegacyDataSourceInfo } from '@/models/datasets'
import type { DataSourceInfo, FileItem, FullDocumentDetail, LegacyDataSourceInfo } from '@/models/datasets'
import { RiArrowLeftLine, RiLayoutLeft2Line, RiLayoutRight2Line } from '@remixicon/react'
import { useRouter } from 'next/navigation'
import * as React from 'react'
@ -256,7 +256,7 @@ const DocumentDetail: FC<DocumentDetailProps> = ({ datasetId, documentId }) => {
className="mr-2 mt-3"
datasetId={datasetId}
documentId={documentId}
docDetail={{ ...documentDetail, ...documentMetadata, doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type } as any}
docDetail={{ ...documentDetail, ...documentMetadata, doc_type: documentMetadata?.doc_type === 'others' ? '' : documentMetadata?.doc_type } as FullDocumentDetail}
/>
</FloatRightContainer>
</div>

View File

@ -0,0 +1,503 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { IndexingType } from '../../create/step-two'
import NewSegmentModal from './new-segment'
// Mock next/navigation
vi.mock('next/navigation', () => ({
useParams: () => ({
datasetId: 'test-dataset-id',
documentId: 'test-document-id',
}),
}))
// Mock ToastContext
const mockNotify = vi.fn()
vi.mock('use-context-selector', async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>
return {
...actual,
useContext: () => ({ notify: mockNotify }),
}
})
// Mock dataset detail context
let mockIndexingTechnique = IndexingType.QUALIFIED
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: (selector: (state: { dataset: { indexing_technique: string } }) => unknown) => {
return selector({ dataset: { indexing_technique: mockIndexingTechnique } })
},
}))
// Mock segment list context
let mockFullScreen = false
const mockToggleFullScreen = vi.fn()
vi.mock('./completed', () => ({
useSegmentListContext: (selector: (state: { fullScreen: boolean, toggleFullScreen: () => void }) => unknown) => {
const state = {
fullScreen: mockFullScreen,
toggleFullScreen: mockToggleFullScreen,
}
return selector(state)
},
}))
// Mock useAddSegment
const mockAddSegment = vi.fn()
vi.mock('@/service/knowledge/use-segment', () => ({
useAddSegment: () => ({
mutateAsync: mockAddSegment,
}),
}))
// Mock app store
vi.mock('@/app/components/app/store', () => ({
useStore: () => ({ appSidebarExpand: 'expand' }),
}))
// Mock child components
vi.mock('./completed/common/action-buttons', () => ({
default: ({ handleCancel, handleSave, loading, actionType }: { handleCancel: () => void, handleSave: () => void, loading: boolean, actionType: string }) => (
<div data-testid="action-buttons">
<button onClick={handleCancel} data-testid="cancel-btn">Cancel</button>
<button onClick={handleSave} disabled={loading} data-testid="save-btn">
{loading ? 'Saving...' : 'Save'}
</button>
<span data-testid="action-type">{actionType}</span>
</div>
),
}))
vi.mock('./completed/common/add-another', () => ({
default: ({ isChecked, onCheck, className }: { isChecked: boolean, onCheck: () => void, className?: string }) => (
<div data-testid="add-another" className={className}>
<input
type="checkbox"
checked={isChecked}
onChange={onCheck}
data-testid="add-another-checkbox"
/>
</div>
),
}))
vi.mock('./completed/common/chunk-content', () => ({
default: ({ docForm, question, answer, onQuestionChange, onAnswerChange, isEditMode }: { docForm: string, question: string, answer: string, onQuestionChange: (v: string) => void, onAnswerChange: (v: string) => void, isEditMode: boolean }) => (
<div data-testid="chunk-content">
<input
data-testid="question-input"
value={question}
onChange={e => onQuestionChange(e.target.value)}
placeholder={docForm === ChunkingMode.qa ? 'Question' : 'Content'}
/>
{docForm === ChunkingMode.qa && (
<input
data-testid="answer-input"
value={answer}
onChange={e => onAnswerChange(e.target.value)}
placeholder="Answer"
/>
)}
<span data-testid="edit-mode">{isEditMode ? 'editing' : 'viewing'}</span>
</div>
),
}))
vi.mock('./completed/common/dot', () => ({
default: () => <span data-testid="dot"></span>,
}))
vi.mock('./completed/common/keywords', () => ({
default: ({ keywords, onKeywordsChange, _isEditMode, _actionType }: { keywords: string[], onKeywordsChange: (v: string[]) => void, _isEditMode?: boolean, _actionType?: string }) => (
<div data-testid="keywords">
<input
data-testid="keywords-input"
value={keywords.join(',')}
onChange={e => onKeywordsChange(e.target.value.split(',').filter(Boolean))}
/>
</div>
),
}))
vi.mock('./completed/common/segment-index-tag', () => ({
SegmentIndexTag: ({ label }: { label: string }) => <span data-testid="segment-index-tag">{label}</span>,
}))
vi.mock('@/app/components/datasets/common/image-uploader/image-uploader-in-chunk', () => ({
default: ({ onChange }: { value?: unknown[], onChange: (v: { uploadedId: string }[]) => void }) => (
<div data-testid="image-uploader">
<button
data-testid="upload-image-btn"
onClick={() => onChange([{ uploadedId: 'img-1' }])}
>
Upload Image
</button>
</div>
),
}))
describe('NewSegmentModal', () => {
beforeEach(() => {
vi.clearAllMocks()
mockFullScreen = false
mockIndexingTechnique = IndexingType.QUALIFIED
})
const defaultProps = {
onCancel: vi.fn(),
docForm: ChunkingMode.text,
onSave: vi.fn(),
viewNewlyAddedChunk: vi.fn(),
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render title text', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByText(/segment\.addChunk/i)).toBeInTheDocument()
})
it('should render chunk content component', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('chunk-content')).toBeInTheDocument()
})
it('should render image uploader', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('image-uploader')).toBeInTheDocument()
})
it('should render segment index tag', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('segment-index-tag')).toBeInTheDocument()
})
it('should render dot separator', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('dot')).toBeInTheDocument()
})
})
// Keywords display
describe('Keywords', () => {
it('should show keywords component when indexing is ECONOMICAL', () => {
// Arrange
mockIndexingTechnique = IndexingType.ECONOMICAL
// Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('keywords')).toBeInTheDocument()
})
it('should not show keywords when indexing is QUALIFIED', () => {
// Arrange
mockIndexingTechnique = IndexingType.QUALIFIED
// Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.queryByTestId('keywords')).not.toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onCancel when close button is clicked', () => {
// Arrange
const mockOnCancel = vi.fn()
const { container } = render(<NewSegmentModal {...defaultProps} onCancel={mockOnCancel} />)
// Act - find and click close button (RiCloseLine icon wrapper)
const closeButtons = container.querySelectorAll('.cursor-pointer')
// The close button is the second cursor-pointer element
if (closeButtons.length > 1)
fireEvent.click(closeButtons[1])
// Assert
expect(mockOnCancel).toHaveBeenCalled()
})
it('should update question when typing', () => {
// Arrange
render(<NewSegmentModal {...defaultProps} />)
const questionInput = screen.getByTestId('question-input')
// Act
fireEvent.change(questionInput, { target: { value: 'New question content' } })
// Assert
expect(questionInput).toHaveValue('New question content')
})
it('should update answer when docForm is QA and typing', () => {
// Arrange
render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.qa} />)
const answerInput = screen.getByTestId('answer-input')
// Act
fireEvent.change(answerInput, { target: { value: 'New answer content' } })
// Assert
expect(answerInput).toHaveValue('New answer content')
})
it('should toggle add another checkbox', () => {
// Arrange
render(<NewSegmentModal {...defaultProps} />)
const checkbox = screen.getByTestId('add-another-checkbox')
// Act
fireEvent.click(checkbox)
// Assert - checkbox state should toggle
expect(checkbox).toBeInTheDocument()
})
})
// Save validation
describe('Save Validation', () => {
it('should show error when content is empty for text mode', async () => {
// Arrange
render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.text} />)
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
})
it('should show error when question is empty for QA mode', async () => {
// Arrange
render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.qa} />)
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
})
it('should show error when answer is empty for QA mode', async () => {
// Arrange
render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.qa} />)
fireEvent.change(screen.getByTestId('question-input'), { target: { value: 'Question' } })
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
)
})
})
})
// Successful save
describe('Successful Save', () => {
it('should call addSegment when valid content is provided for text mode', async () => {
// Arrange
mockAddSegment.mockImplementation((_params, options) => {
options.onSuccess()
options.onSettled()
return Promise.resolve()
})
render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.text} />)
fireEvent.change(screen.getByTestId('question-input'), { target: { value: 'Valid content' } })
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockAddSegment).toHaveBeenCalledWith(
expect.objectContaining({
datasetId: 'test-dataset-id',
documentId: 'test-document-id',
body: expect.objectContaining({
content: 'Valid content',
}),
}),
expect.any(Object),
)
})
})
it('should show success notification after save', async () => {
// Arrange
mockAddSegment.mockImplementation((_params, options) => {
options.onSuccess()
options.onSettled()
return Promise.resolve()
})
render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.text} />)
fireEvent.change(screen.getByTestId('question-input'), { target: { value: 'Valid content' } })
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
expect.objectContaining({
type: 'success',
}),
)
})
})
})
// Full screen mode
describe('Full Screen Mode', () => {
it('should apply full screen styling when fullScreen is true', () => {
// Arrange
mockFullScreen = true
// Act
const { container } = render(<NewSegmentModal {...defaultProps} />)
// Assert
const header = container.querySelector('.border-divider-subtle')
expect(header).toBeInTheDocument()
})
it('should show action buttons in header when fullScreen', () => {
// Arrange
mockFullScreen = true
// Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('action-buttons')).toBeInTheDocument()
})
it('should show add another in header when fullScreen', () => {
// Arrange
mockFullScreen = true
// Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('add-another')).toBeInTheDocument()
})
it('should call toggleFullScreen when expand button is clicked', () => {
// Arrange
const { container } = render(<NewSegmentModal {...defaultProps} />)
// Act - click the expand button (first cursor-pointer)
const expandButtons = container.querySelectorAll('.cursor-pointer')
if (expandButtons.length > 0)
fireEvent.click(expandButtons[0])
// Assert
expect(mockToggleFullScreen).toHaveBeenCalled()
})
})
// Props
describe('Props', () => {
it('should pass actionType add to ActionButtons', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('action-type')).toHaveTextContent('add')
})
it('should pass isEditMode true to ChunkContent', () => {
// Arrange & Act
render(<NewSegmentModal {...defaultProps} />)
// Assert
expect(screen.getByTestId('edit-mode')).toHaveTextContent('editing')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle keyword changes for ECONOMICAL indexing', () => {
// Arrange
mockIndexingTechnique = IndexingType.ECONOMICAL
render(<NewSegmentModal {...defaultProps} />)
// Act
fireEvent.change(screen.getByTestId('keywords-input'), {
target: { value: 'keyword1,keyword2' },
})
// Assert
expect(screen.getByTestId('keywords-input')).toHaveValue('keyword1,keyword2')
})
it('should handle image upload', () => {
// Arrange
render(<NewSegmentModal {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('upload-image-btn'))
// Assert - image uploader should be rendered
expect(screen.getByTestId('image-uploader')).toBeInTheDocument()
})
it('should maintain structure when rerendered with different docForm', () => {
// Arrange
const { rerender } = render(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.text} />)
// Act
rerender(<NewSegmentModal {...defaultProps} docForm={ChunkingMode.qa} />)
// Assert
expect(screen.getByTestId('answer-input')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,350 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Plan } from '@/app/components/billing/type'
import SegmentAdd, { ProcessStatus } from './index'
// Mock provider context
let mockPlan = { type: Plan.professional }
let mockEnableBilling = true
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
plan: mockPlan,
enableBilling: mockEnableBilling,
}),
}))
// Mock PlanUpgradeModal
vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
default: ({ show, onClose, title, description }: { show: boolean, onClose: () => void, title?: string, description?: string }) => (
show
? (
<div data-testid="plan-upgrade-modal">
<span data-testid="modal-title">{title}</span>
<span data-testid="modal-description">{description}</span>
<button onClick={onClose} data-testid="close-modal">Close</button>
</div>
)
: null
),
}))
// Mock Popover
vi.mock('@/app/components/base/popover', () => ({
default: ({ htmlContent, btnElement, disabled }: { htmlContent: React.ReactNode, btnElement: React.ReactNode, disabled?: boolean }) => (
<div data-testid="popover">
<button data-testid="popover-btn" disabled={disabled}>
{btnElement}
</button>
<div data-testid="popover-content">{htmlContent}</div>
</div>
),
}))
describe('SegmentAdd', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPlan = { type: Plan.professional }
mockEnableBilling = true
})
const defaultProps = {
importStatus: undefined as ProcessStatus | string | undefined,
clearProcessStatus: vi.fn(),
showNewSegmentModal: vi.fn(),
showBatchModal: vi.fn(),
embedding: false,
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<SegmentAdd {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render add button when no importStatus', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} />)
// Assert
expect(screen.getByText(/list\.action\.addButton/i)).toBeInTheDocument()
})
it('should render popover for batch add', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} />)
// Assert
expect(screen.getByTestId('popover')).toBeInTheDocument()
})
})
// Import Status displays
describe('Import Status Display', () => {
it('should show processing indicator when status is WAITING', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.WAITING} />)
// Assert
expect(screen.getByText(/list\.batchModal\.processing/i)).toBeInTheDocument()
})
it('should show processing indicator when status is PROCESSING', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.PROCESSING} />)
// Assert
expect(screen.getByText(/list\.batchModal\.processing/i)).toBeInTheDocument()
})
it('should show completed status with ok button', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.COMPLETED} />)
// Assert
expect(screen.getByText(/list\.batchModal\.completed/i)).toBeInTheDocument()
expect(screen.getByText(/list\.batchModal\.ok/i)).toBeInTheDocument()
})
it('should show error status with ok button', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.ERROR} />)
// Assert
expect(screen.getByText(/list\.batchModal\.error/i)).toBeInTheDocument()
expect(screen.getByText(/list\.batchModal\.ok/i)).toBeInTheDocument()
})
it('should not show add button when importStatus is set', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.PROCESSING} />)
// Assert
expect(screen.queryByText(/list\.action\.addButton/i)).not.toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call showNewSegmentModal when add button is clicked', () => {
// Arrange
const mockShowNewSegmentModal = vi.fn()
render(<SegmentAdd {...defaultProps} showNewSegmentModal={mockShowNewSegmentModal} />)
// Act
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
// Assert
expect(mockShowNewSegmentModal).toHaveBeenCalledTimes(1)
})
it('should call clearProcessStatus when ok is clicked on completed status', () => {
// Arrange
const mockClearProcessStatus = vi.fn()
render(
<SegmentAdd
{...defaultProps}
importStatus={ProcessStatus.COMPLETED}
clearProcessStatus={mockClearProcessStatus}
/>,
)
// Act
fireEvent.click(screen.getByText(/list\.batchModal\.ok/i))
// Assert
expect(mockClearProcessStatus).toHaveBeenCalledTimes(1)
})
it('should call clearProcessStatus when ok is clicked on error status', () => {
// Arrange
const mockClearProcessStatus = vi.fn()
render(
<SegmentAdd
{...defaultProps}
importStatus={ProcessStatus.ERROR}
clearProcessStatus={mockClearProcessStatus}
/>,
)
// Act
fireEvent.click(screen.getByText(/list\.batchModal\.ok/i))
// Assert
expect(mockClearProcessStatus).toHaveBeenCalledTimes(1)
})
it('should render batch add option in popover', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} />)
// Assert
expect(screen.getByText(/list\.action\.batchAdd/i)).toBeInTheDocument()
})
it('should call showBatchModal when batch add is clicked', () => {
// Arrange
const mockShowBatchModal = vi.fn()
render(<SegmentAdd {...defaultProps} showBatchModal={mockShowBatchModal} />)
// Act
fireEvent.click(screen.getByText(/list\.action\.batchAdd/i))
// Assert
expect(mockShowBatchModal).toHaveBeenCalledTimes(1)
})
})
// Disabled state (embedding)
describe('Embedding State', () => {
it('should disable add button when embedding is true', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} embedding={true} />)
// Assert
const addButton = screen.getByText(/list\.action\.addButton/i).closest('button')
expect(addButton).toBeDisabled()
})
it('should disable popover button when embedding is true', () => {
// Arrange & Act
render(<SegmentAdd {...defaultProps} embedding={true} />)
// Assert
expect(screen.getByTestId('popover-btn')).toBeDisabled()
})
it('should apply disabled styling when embedding is true', () => {
// Arrange & Act
const { container } = render(<SegmentAdd {...defaultProps} embedding={true} />)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('border-components-button-secondary-border-disabled')
})
})
// Plan upgrade modal
describe('Plan Upgrade Modal', () => {
it('should show plan upgrade modal when sandbox user tries to add', () => {
// Arrange
mockPlan = { type: Plan.sandbox }
render(<SegmentAdd {...defaultProps} />)
// Act
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
// Assert
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
})
it('should not call showNewSegmentModal for sandbox users', () => {
// Arrange
mockPlan = { type: Plan.sandbox }
const mockShowNewSegmentModal = vi.fn()
render(<SegmentAdd {...defaultProps} showNewSegmentModal={mockShowNewSegmentModal} />)
// Act
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
// Assert
expect(mockShowNewSegmentModal).not.toHaveBeenCalled()
})
it('should allow add when billing is disabled regardless of plan', () => {
// Arrange
mockPlan = { type: Plan.sandbox }
mockEnableBilling = false
const mockShowNewSegmentModal = vi.fn()
render(<SegmentAdd {...defaultProps} showNewSegmentModal={mockShowNewSegmentModal} />)
// Act
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
// Assert
expect(mockShowNewSegmentModal).toHaveBeenCalledTimes(1)
})
it('should close plan upgrade modal when close button is clicked', () => {
// Arrange
mockPlan = { type: Plan.sandbox }
render(<SegmentAdd {...defaultProps} />)
// Show modal
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
// Act
fireEvent.click(screen.getByTestId('close-modal'))
// Assert
expect(screen.queryByTestId('plan-upgrade-modal')).not.toBeInTheDocument()
})
})
// Progress bar width tests
describe('Progress Bar', () => {
it('should show 3/12 width progress bar for WAITING status', () => {
// Arrange & Act
const { container } = render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.WAITING} />)
// Assert
const progressBar = container.querySelector('.w-3\\/12')
expect(progressBar).toBeInTheDocument()
})
it('should show 2/3 width progress bar for PROCESSING status', () => {
// Arrange & Act
const { container } = render(<SegmentAdd {...defaultProps} importStatus={ProcessStatus.PROCESSING} />)
// Assert
const progressBar = container.querySelector('.w-2\\/3')
expect(progressBar).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle unknown importStatus string', () => {
// Arrange & Act - pass unknown status
const { container } = render(<SegmentAdd {...defaultProps} importStatus="unknown" />)
// Assert - empty fragment is rendered for unknown status (container exists but has no visible content)
expect(container).toBeInTheDocument()
expect(container.textContent).toBe('')
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<SegmentAdd {...defaultProps} />)
// Act
rerender(<SegmentAdd {...defaultProps} embedding={true} />)
// Assert
const addButton = screen.getByText(/list\.action\.addButton/i).closest('button')
expect(addButton).toBeDisabled()
})
it('should handle callback change', () => {
// Arrange
const mockShowNewSegmentModal1 = vi.fn()
const mockShowNewSegmentModal2 = vi.fn()
const { rerender } = render(
<SegmentAdd {...defaultProps} showNewSegmentModal={mockShowNewSegmentModal1} />,
)
// Act
rerender(<SegmentAdd {...defaultProps} showNewSegmentModal={mockShowNewSegmentModal2} />)
fireEvent.click(screen.getByText(/list\.action\.addButton/i))
// Assert
expect(mockShowNewSegmentModal1).not.toHaveBeenCalled()
expect(mockShowNewSegmentModal2).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -0,0 +1,374 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import DocumentSettings from './document-settings'
// Mock next/navigation
const mockPush = vi.fn()
const mockBack = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
back: mockBack,
}),
}))
// Mock use-context-selector
vi.mock('use-context-selector', async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>
return {
...actual,
useContext: () => ({
indexingTechnique: 'qualified',
dataset: { id: 'dataset-1' },
}),
}
})
// Mock hooks
const mockInvalidDocumentList = vi.fn()
const mockInvalidDocumentDetail = vi.fn()
let mockDocumentDetail: Record<string, unknown> | null = {
name: 'test-document',
data_source_type: 'upload_file',
data_source_info: {
upload_file: { id: 'file-1', name: 'test.pdf' },
},
}
let mockError: Error | null = null
vi.mock('@/service/knowledge/use-document', () => ({
useDocumentDetail: () => ({
data: mockDocumentDetail,
error: mockError,
}),
useInvalidDocumentList: () => mockInvalidDocumentList,
useInvalidDocumentDetail: () => mockInvalidDocumentDetail,
}))
// Mock useDefaultModel
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useDefaultModel: () => ({
data: { model: 'text-embedding-ada-002' },
}),
}))
// Mock child components
vi.mock('@/app/components/base/app-unavailable', () => ({
default: ({ code, unknownReason }: { code?: number, unknownReason?: string }) => (
<div data-testid="app-unavailable">
<span data-testid="error-code">{code}</span>
<span data-testid="error-reason">{unknownReason}</span>
</div>
),
}))
vi.mock('@/app/components/base/loading', () => ({
default: ({ type }: { type?: string }) => (
<div data-testid="loading" data-type={type}>Loading...</div>
),
}))
vi.mock('@/app/components/datasets/create/step-two', () => ({
default: ({
isAPIKeySet,
onSetting,
datasetId,
dataSourceType,
files,
onSave,
onCancel,
isSetting,
}: {
isAPIKeySet?: boolean
onSetting?: () => void
datasetId?: string
dataSourceType?: string
files?: unknown[]
onSave?: () => void
onCancel?: () => void
isSetting?: boolean
}) => (
<div data-testid="step-two">
<span data-testid="api-key-set">{isAPIKeySet ? 'true' : 'false'}</span>
<span data-testid="dataset-id">{datasetId}</span>
<span data-testid="data-source-type">{dataSourceType}</span>
<span data-testid="is-setting">{isSetting ? 'true' : 'false'}</span>
<span data-testid="files-count">{files?.length || 0}</span>
<button onClick={onSetting} data-testid="setting-btn">Setting</button>
<button onClick={onSave} data-testid="save-btn">Save</button>
<button onClick={onCancel} data-testid="cancel-btn">Cancel</button>
</div>
),
}))
vi.mock('@/app/components/header/account-setting', () => ({
default: ({ activeTab, onCancel }: { activeTab?: string, onCancel?: () => void }) => (
<div data-testid="account-setting">
<span data-testid="active-tab">{activeTab}</span>
<button onClick={onCancel} data-testid="close-setting">Close</button>
</div>
),
}))
describe('DocumentSettings', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDocumentDetail = {
name: 'test-document',
data_source_type: 'upload_file',
data_source_info: {
upload_file: { id: 'file-1', name: 'test.pdf' },
},
}
mockError = null
})
const defaultProps = {
datasetId: 'dataset-1',
documentId: 'document-1',
}
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<DocumentSettings {...defaultProps} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render StepTwo component when data is loaded', () => {
// Arrange & Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('step-two')).toBeInTheDocument()
})
it('should render loading when documentDetail is not available', () => {
// Arrange
mockDocumentDetail = null
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('loading')).toBeInTheDocument()
})
it('should render AppUnavailable when error occurs', () => {
// Arrange
mockError = new Error('Error loading document')
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('app-unavailable')).toBeInTheDocument()
expect(screen.getByTestId('error-code')).toHaveTextContent('500')
})
})
// Props passing
describe('Props Passing', () => {
it('should pass datasetId to StepTwo', () => {
// Arrange & Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('dataset-id')).toHaveTextContent('dataset-1')
})
it('should pass isSetting true to StepTwo', () => {
// Arrange & Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('is-setting')).toHaveTextContent('true')
})
it('should pass isAPIKeySet when embedding model is available', () => {
// Arrange & Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('api-key-set')).toHaveTextContent('true')
})
it('should pass data source type to StepTwo', () => {
// Arrange & Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('data-source-type')).toHaveTextContent('upload_file')
})
})
// User Interactions
describe('User Interactions', () => {
it('should call router.back when cancel is clicked', () => {
// Arrange
render(<DocumentSettings {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('cancel-btn'))
// Assert
expect(mockBack).toHaveBeenCalled()
})
it('should navigate to document page when save is clicked', () => {
// Arrange
render(<DocumentSettings {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('save-btn'))
// Assert
expect(mockInvalidDocumentList).toHaveBeenCalled()
expect(mockInvalidDocumentDetail).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents/document-1')
})
it('should show AccountSetting modal when setting button is clicked', () => {
// Arrange
render(<DocumentSettings {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('setting-btn'))
// Assert
expect(screen.getByTestId('account-setting')).toBeInTheDocument()
})
it('should hide AccountSetting modal when close is clicked', async () => {
// Arrange
render(<DocumentSettings {...defaultProps} />)
fireEvent.click(screen.getByTestId('setting-btn'))
expect(screen.getByTestId('account-setting')).toBeInTheDocument()
// Act
fireEvent.click(screen.getByTestId('close-setting'))
// Assert
expect(screen.queryByTestId('account-setting')).not.toBeInTheDocument()
})
})
// Data source types
describe('Data Source Types', () => {
it('should handle legacy upload_file data source', () => {
// Arrange
mockDocumentDetail = {
name: 'test-document',
data_source_type: 'upload_file',
data_source_info: {
upload_file: { id: 'file-1', name: 'test.pdf' },
},
}
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('files-count')).toHaveTextContent('1')
})
it('should handle website crawl data source', () => {
// Arrange
mockDocumentDetail = {
name: 'test-website',
data_source_type: 'website_crawl',
data_source_info: {
title: 'Test Page',
source_url: 'https://example.com',
content: 'Page content',
description: 'Page description',
},
}
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('data-source-type')).toHaveTextContent('website_crawl')
})
it('should handle local file data source', () => {
// Arrange
mockDocumentDetail = {
name: 'local-file',
data_source_type: 'upload_file',
data_source_info: {
related_id: 'file-id',
transfer_method: 'local',
name: 'local-file.pdf',
extension: 'pdf',
},
}
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('files-count')).toHaveTextContent('1')
})
it('should handle online document (Notion) data source', () => {
// Arrange
mockDocumentDetail = {
name: 'notion-page',
data_source_type: 'notion_import',
data_source_info: {
workspace_id: 'ws-1',
credential_id: 'cred-1',
page: {
page_id: 'page-1',
page_name: 'Test Page',
page_icon: '📄',
type: 'page',
},
},
}
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('data-source-type')).toHaveTextContent('notion_import')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle undefined data_source_info', () => {
// Arrange
mockDocumentDetail = {
name: 'test-document',
data_source_type: 'upload_file',
data_source_info: undefined,
}
// Act
render(<DocumentSettings {...defaultProps} />)
// Assert
expect(screen.getByTestId('files-count')).toHaveTextContent('0')
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(
<DocumentSettings datasetId="dataset-1" documentId="doc-1" />,
)
// Act
rerender(<DocumentSettings datasetId="dataset-2" documentId="doc-2" />)
// Assert
expect(screen.getByTestId('step-two')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,143 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Settings from './index'
// Mock the dataset detail context
let mockRuntimeMode: string | undefined = 'general'
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: (selector: (state: { dataset: { runtime_mode: string | undefined } }) => unknown) => {
return selector({ dataset: { runtime_mode: mockRuntimeMode } })
},
}))
// Mock child components
vi.mock('./document-settings', () => ({
default: ({ datasetId, documentId }: { datasetId: string, documentId: string }) => (
<div data-testid="document-settings">
DocumentSettings -
{' '}
{datasetId}
{' '}
-
{' '}
{documentId}
</div>
),
}))
vi.mock('./pipeline-settings', () => ({
default: ({ datasetId, documentId }: { datasetId: string, documentId: string }) => (
<div data-testid="pipeline-settings">
PipelineSettings -
{' '}
{datasetId}
{' '}
-
{' '}
{documentId}
</div>
),
}))
describe('Settings', () => {
beforeEach(() => {
vi.clearAllMocks()
mockRuntimeMode = 'general'
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(
<Settings datasetId="dataset-1" documentId="doc-1" />,
)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
})
// Conditional rendering tests
describe('Conditional Rendering', () => {
it('should render DocumentSettings when runtimeMode is general', () => {
// Arrange
mockRuntimeMode = 'general'
// Act
render(<Settings datasetId="dataset-1" documentId="doc-1" />)
// Assert
expect(screen.getByTestId('document-settings')).toBeInTheDocument()
expect(screen.queryByTestId('pipeline-settings')).not.toBeInTheDocument()
})
it('should render PipelineSettings when runtimeMode is not general', () => {
// Arrange
mockRuntimeMode = 'pipeline'
// Act
render(<Settings datasetId="dataset-1" documentId="doc-1" />)
// Assert
expect(screen.getByTestId('pipeline-settings')).toBeInTheDocument()
expect(screen.queryByTestId('document-settings')).not.toBeInTheDocument()
})
})
// Props passing tests
describe('Props', () => {
it('should pass datasetId and documentId to DocumentSettings', () => {
// Arrange
mockRuntimeMode = 'general'
// Act
render(<Settings datasetId="test-dataset" documentId="test-document" />)
// Assert
expect(screen.getByText(/test-dataset/)).toBeInTheDocument()
expect(screen.getByText(/test-document/)).toBeInTheDocument()
})
it('should pass datasetId and documentId to PipelineSettings', () => {
// Arrange
mockRuntimeMode = 'pipeline'
// Act
render(<Settings datasetId="test-dataset" documentId="test-document" />)
// Assert
expect(screen.getByText(/test-dataset/)).toBeInTheDocument()
expect(screen.getByText(/test-document/)).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle undefined runtimeMode as non-general', () => {
// Arrange
mockRuntimeMode = undefined
// Act
render(<Settings datasetId="dataset-1" documentId="doc-1" />)
// Assert - undefined !== 'general', so PipelineSettings should render
expect(screen.getByTestId('pipeline-settings')).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
mockRuntimeMode = 'general'
const { rerender } = render(
<Settings datasetId="dataset-1" documentId="doc-1" />,
)
// Act
rerender(<Settings datasetId="dataset-2" documentId="doc-2" />)
// Assert
expect(screen.getByText(/dataset-2/)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,154 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import LeftHeader from './left-header'
// Mock next/navigation
const mockBack = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
back: mockBack,
}),
}))
describe('LeftHeader', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<LeftHeader title="Test Title" />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render the title', () => {
// Arrange & Act
render(<LeftHeader title="My Document Title" />)
// Assert
expect(screen.getByText('My Document Title')).toBeInTheDocument()
})
it('should render the process documents text', () => {
// Arrange & Act
render(<LeftHeader title="Test" />)
// Assert - i18n key format
expect(screen.getByText(/addDocuments\.steps\.processDocuments/i)).toBeInTheDocument()
})
it('should render back button', () => {
// Arrange & Act
render(<LeftHeader title="Test" />)
// Assert
const backButton = screen.getByRole('button')
expect(backButton).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call router.back when back button is clicked', () => {
// Arrange
render(<LeftHeader title="Test" />)
// Act
const backButton = screen.getByRole('button')
fireEvent.click(backButton)
// Assert
expect(mockBack).toHaveBeenCalledTimes(1)
})
it('should call router.back multiple times on multiple clicks', () => {
// Arrange
render(<LeftHeader title="Test" />)
// Act
const backButton = screen.getByRole('button')
fireEvent.click(backButton)
fireEvent.click(backButton)
// Assert
expect(mockBack).toHaveBeenCalledTimes(2)
})
})
// Props tests
describe('Props', () => {
it('should render different titles', () => {
// Arrange
const { rerender } = render(<LeftHeader title="First Title" />)
expect(screen.getByText('First Title')).toBeInTheDocument()
// Act
rerender(<LeftHeader title="Second Title" />)
// Assert
expect(screen.getByText('Second Title')).toBeInTheDocument()
})
})
// Styling tests
describe('Styling', () => {
it('should have back button with proper styling', () => {
// Arrange & Act
render(<LeftHeader title="Test" />)
// Assert
const backButton = screen.getByRole('button')
expect(backButton).toHaveClass('absolute')
expect(backButton).toHaveClass('rounded-full')
})
it('should render title with gradient background styling', () => {
// Arrange & Act
const { container } = render(<LeftHeader title="Test" />)
// Assert
const titleElement = container.querySelector('.bg-pipeline-add-documents-title-bg')
expect(titleElement).toBeInTheDocument()
})
})
// Accessibility tests
describe('Accessibility', () => {
it('should have aria-label on back button', () => {
// Arrange & Act
render(<LeftHeader title="Test" />)
// Assert
const backButton = screen.getByRole('button')
expect(backButton).toHaveAttribute('aria-label')
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty title', () => {
// Arrange & Act
const { container } = render(<LeftHeader title="" />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should maintain structure when rerendered', () => {
// Arrange
const { rerender } = render(<LeftHeader title="Test" />)
// Act
rerender(<LeftHeader title="Updated Test" />)
// Assert
expect(screen.getByText('Updated Test')).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,158 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Actions from './actions'
describe('Actions', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
const { container } = render(<Actions onProcess={vi.fn()} />)
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should render save and process button', () => {
// Arrange & Act
render(<Actions onProcess={vi.fn()} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render button with translated text', () => {
// Arrange & Act
render(<Actions onProcess={vi.fn()} />)
// Assert - i18n key format
expect(screen.getByText(/operations\.saveAndProcess/i)).toBeInTheDocument()
})
it('should render with correct container styling', () => {
// Arrange & Act
const { container } = render(<Actions onProcess={vi.fn()} />)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex')
expect(wrapper).toHaveClass('items-center')
expect(wrapper).toHaveClass('justify-end')
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onProcess when button is clicked', () => {
// Arrange
const mockOnProcess = vi.fn()
render(<Actions onProcess={mockOnProcess} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnProcess).toHaveBeenCalledTimes(1)
})
it('should not call onProcess when button is disabled', () => {
// Arrange
const mockOnProcess = vi.fn()
render(<Actions onProcess={mockOnProcess} runDisabled={true} />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnProcess).not.toHaveBeenCalled()
})
})
// Props tests
describe('Props', () => {
it('should disable button when runDisabled is true', () => {
// Arrange & Act
render(<Actions onProcess={vi.fn()} runDisabled={true} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
it('should enable button when runDisabled is false', () => {
// Arrange & Act
render(<Actions onProcess={vi.fn()} runDisabled={false} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
it('should enable button when runDisabled is undefined (default)', () => {
// Arrange & Act
render(<Actions onProcess={vi.fn()} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
})
// Button variant tests
describe('Button Styling', () => {
it('should render button with primary variant', () => {
// Arrange & Act
render(<Actions onProcess={vi.fn()} />)
// Assert - primary variant buttons have specific classes
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle multiple rapid clicks', () => {
// Arrange
const mockOnProcess = vi.fn()
render(<Actions onProcess={mockOnProcess} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.click(button)
// Assert
expect(mockOnProcess).toHaveBeenCalledTimes(3)
})
it('should maintain structure when rerendered', () => {
// Arrange
const mockOnProcess = vi.fn()
const { rerender } = render(<Actions onProcess={mockOnProcess} />)
// Act
rerender(<Actions onProcess={mockOnProcess} runDisabled={true} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
it('should handle callback change', () => {
// Arrange
const mockOnProcess1 = vi.fn()
const mockOnProcess2 = vi.fn()
const { rerender } = render(<Actions onProcess={mockOnProcess1} />)
// Act
rerender(<Actions onProcess={mockOnProcess2} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockOnProcess1).not.toHaveBeenCalled()
expect(mockOnProcess2).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1818,11 +1818,6 @@
"count": 1
}
},
"app/components/datasets/documents/detail/index.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"app/components/datasets/documents/detail/metadata/index.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 4