mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
test: add tests for dataset document detail (#31274)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
@ -0,0 +1,214 @@
|
||||
import type { SortType } from '@/service/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import DocumentsHeader from './documents-header'
|
||||
|
||||
// Mock the context hooks
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||
}))
|
||||
|
||||
// Mock child components that require API calls
|
||||
vi.mock('@/app/components/datasets/common/document-status-with-action/auto-disabled-document', () => ({
|
||||
default: () => <div data-testid="auto-disabled-document">AutoDisabledDocument</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/document-status-with-action/index-failed', () => ({
|
||||
default: () => <div data-testid="index-failed">IndexFailed</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer', () => ({
|
||||
default: ({ onClose }: { onClose: () => void }) => (
|
||||
<div data-testid="metadata-drawer">
|
||||
<button onClick={onClose}>Close</button>
|
||||
MetadataDrawer
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('DocumentsHeader', () => {
|
||||
const defaultProps = {
|
||||
datasetId: 'dataset-123',
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
embeddingAvailable: true,
|
||||
isFreePlan: false,
|
||||
statusFilterValue: 'all',
|
||||
sortValue: 'created_at' as SortType,
|
||||
inputValue: '',
|
||||
onStatusFilterChange: vi.fn(),
|
||||
onStatusFilterClear: vi.fn(),
|
||||
onSortChange: vi.fn(),
|
||||
onInputChange: vi.fn(),
|
||||
isShowEditMetadataModal: false,
|
||||
showEditMetadataModal: vi.fn(),
|
||||
hideEditMetadataModal: vi.fn(),
|
||||
datasetMetaData: [],
|
||||
builtInMetaData: [],
|
||||
builtInEnabled: true,
|
||||
onAddMetaData: vi.fn(),
|
||||
onRenameMetaData: vi.fn(),
|
||||
onDeleteMetaData: vi.fn(),
|
||||
onBuiltInEnabledChange: vi.fn(),
|
||||
onAddDocument: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<DocumentsHeader {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render title', () => {
|
||||
render(<DocumentsHeader {...defaultProps} />)
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(/list\.title/i)
|
||||
})
|
||||
|
||||
it('should render description text', () => {
|
||||
render(<DocumentsHeader {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.desc/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render learn more link', () => {
|
||||
render(<DocumentsHeader {...defaultProps} />)
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveTextContent(/list\.learnMore/i)
|
||||
expect(link).toHaveAttribute('href', expect.stringContaining('use-dify/knowledge'))
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should render filter input', () => {
|
||||
render(<DocumentsHeader {...defaultProps} />)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('AutoDisabledDocument', () => {
|
||||
it('should show AutoDisabledDocument when not free plan', () => {
|
||||
render(<DocumentsHeader {...defaultProps} isFreePlan={false} />)
|
||||
expect(screen.getByTestId('auto-disabled-document')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show AutoDisabledDocument when on free plan', () => {
|
||||
render(<DocumentsHeader {...defaultProps} isFreePlan={true} />)
|
||||
expect(screen.queryByTestId('auto-disabled-document')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('IndexFailed', () => {
|
||||
it('should always show IndexFailed component', () => {
|
||||
render(<DocumentsHeader {...defaultProps} />)
|
||||
expect(screen.getByTestId('index-failed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Embedding Availability', () => {
|
||||
it('should show metadata button when embedding is available', () => {
|
||||
render(<DocumentsHeader {...defaultProps} embeddingAvailable={true} />)
|
||||
expect(screen.getByText(/metadata\.metadata/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show add document button when embedding is available', () => {
|
||||
render(<DocumentsHeader {...defaultProps} embeddingAvailable={true} />)
|
||||
expect(screen.getByText(/list\.addFile/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show warning when embedding is not available', () => {
|
||||
render(<DocumentsHeader {...defaultProps} embeddingAvailable={false} />)
|
||||
expect(screen.queryByText(/metadata\.metadata/i)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/list\.addFile/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add Button Text', () => {
|
||||
it('should show "Add File" for FILE data source', () => {
|
||||
render(<DocumentsHeader {...defaultProps} dataSourceType={DataSourceType.FILE} />)
|
||||
expect(screen.getByText(/list\.addFile/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show "Add Pages" for NOTION data source', () => {
|
||||
render(<DocumentsHeader {...defaultProps} dataSourceType={DataSourceType.NOTION} />)
|
||||
expect(screen.getByText(/list\.addPages/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show "Add Url" for WEB data source', () => {
|
||||
render(<DocumentsHeader {...defaultProps} dataSourceType={DataSourceType.WEB} />)
|
||||
expect(screen.getByText(/list\.addUrl/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Metadata Modal', () => {
|
||||
it('should show metadata drawer when isShowEditMetadataModal is true', () => {
|
||||
render(<DocumentsHeader {...defaultProps} isShowEditMetadataModal={true} />)
|
||||
expect(screen.getByTestId('metadata-drawer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show metadata drawer when isShowEditMetadataModal is false', () => {
|
||||
render(<DocumentsHeader {...defaultProps} isShowEditMetadataModal={false} />)
|
||||
expect(screen.queryByTestId('metadata-drawer')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call showEditMetadataModal when metadata button is clicked', () => {
|
||||
const showEditMetadataModal = vi.fn()
|
||||
render(<DocumentsHeader {...defaultProps} showEditMetadataModal={showEditMetadataModal} />)
|
||||
|
||||
const metadataButton = screen.getByText(/metadata\.metadata/i)
|
||||
fireEvent.click(metadataButton)
|
||||
|
||||
expect(showEditMetadataModal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onAddDocument when add button is clicked', () => {
|
||||
const onAddDocument = vi.fn()
|
||||
render(<DocumentsHeader {...defaultProps} onAddDocument={onAddDocument} />)
|
||||
|
||||
const addButton = screen.getByText(/list\.addFile/i)
|
||||
fireEvent.click(addButton)
|
||||
|
||||
expect(onAddDocument).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onInputChange when typing in search input', () => {
|
||||
const onInputChange = vi.fn()
|
||||
render(<DocumentsHeader {...defaultProps} onInputChange={onInputChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'search query' } })
|
||||
|
||||
expect(onInputChange).toHaveBeenCalledWith('search query')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined dataSourceType', () => {
|
||||
render(<DocumentsHeader {...defaultProps} dataSourceType={undefined} />)
|
||||
// Should default to "Add File" text
|
||||
expect(screen.getByText(/list\.addFile/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty metadata arrays', () => {
|
||||
render(
|
||||
<DocumentsHeader
|
||||
{...defaultProps}
|
||||
isShowEditMetadataModal={true}
|
||||
datasetMetaData={[]}
|
||||
builtInMetaData={[]}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('metadata-drawer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with descending sort order', () => {
|
||||
render(<DocumentsHeader {...defaultProps} sortValue="-created_at" />)
|
||||
// Component should still render correctly
|
||||
expect(screen.getByText(/list\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,95 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import EmptyElement from './empty-element'
|
||||
|
||||
describe('EmptyElement', () => {
|
||||
const defaultProps = {
|
||||
canAdd: true,
|
||||
onClick: vi.fn(),
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<EmptyElement {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.empty\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render title text', () => {
|
||||
render(<EmptyElement {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.empty\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tip text for upload type', () => {
|
||||
render(<EmptyElement {...defaultProps} type="upload" />)
|
||||
expect(screen.getByText(/list\.empty\.upload\.tip/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tip text for sync type', () => {
|
||||
render(<EmptyElement {...defaultProps} type="sync" />)
|
||||
expect(screen.getByText(/list\.empty\.sync\.tip/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should use upload type by default', () => {
|
||||
render(<EmptyElement {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.empty\.upload\.tip/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render FolderPlusIcon for upload type', () => {
|
||||
const { container } = render(<EmptyElement {...defaultProps} type="upload" />)
|
||||
// FolderPlusIcon has specific SVG attributes
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
expect(svgs.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render NotionIcon for sync type', () => {
|
||||
const { container } = render(<EmptyElement {...defaultProps} type="sync" />)
|
||||
// NotionIcon has clipPath
|
||||
const clipPath = container.querySelector('clipPath')
|
||||
expect(clipPath).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add Button', () => {
|
||||
it('should show add button when canAdd is true and type is upload', () => {
|
||||
render(<EmptyElement {...defaultProps} canAdd={true} type="upload" />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getByText(/list\.addFile/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show add button when canAdd is false', () => {
|
||||
render(<EmptyElement {...defaultProps} canAdd={false} type="upload" />)
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show add button for sync type', () => {
|
||||
render(<EmptyElement {...defaultProps} canAdd={true} type="sync" />)
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show add button for sync type even when canAdd is true', () => {
|
||||
render(<EmptyElement canAdd={true} onClick={vi.fn()} type="sync" />)
|
||||
expect(screen.queryByText(/list\.addFile/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onClick when add button is clicked', () => {
|
||||
const handleClick = vi.fn()
|
||||
render(<EmptyElement canAdd={true} onClick={handleClick} type="upload" />)
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle default canAdd value (true)', () => {
|
||||
render(<EmptyElement onClick={vi.fn()} canAdd={true} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,81 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { FolderPlusIcon, NotionIcon, ThreeDotsIcon } from './icons'
|
||||
|
||||
describe('Icons', () => {
|
||||
describe('FolderPlusIcon', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<FolderPlusIcon />)
|
||||
const svg = document.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct dimensions', () => {
|
||||
render(<FolderPlusIcon />)
|
||||
const svg = document.querySelector('svg')
|
||||
expect(svg).toHaveAttribute('width', '20')
|
||||
expect(svg).toHaveAttribute('height', '20')
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
render(<FolderPlusIcon className="custom-class" />)
|
||||
const svg = document.querySelector('svg')
|
||||
expect(svg).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should have empty className by default', () => {
|
||||
render(<FolderPlusIcon />)
|
||||
const svg = document.querySelector('svg')
|
||||
expect(svg).toHaveAttribute('class', '')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ThreeDotsIcon', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<ThreeDotsIcon />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct dimensions', () => {
|
||||
const { container } = render(<ThreeDotsIcon />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toHaveAttribute('width', '16')
|
||||
expect(svg).toHaveAttribute('height', '16')
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<ThreeDotsIcon className="custom-class" />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toHaveClass('custom-class')
|
||||
})
|
||||
})
|
||||
|
||||
describe('NotionIcon', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<NotionIcon />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct dimensions', () => {
|
||||
const { container } = render(<NotionIcon />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toHaveAttribute('width', '20')
|
||||
expect(svg).toHaveAttribute('height', '20')
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<NotionIcon className="custom-class" />)
|
||||
const svg = container.querySelector('svg')
|
||||
expect(svg).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should contain clipPath definition', () => {
|
||||
const { container } = render(<NotionIcon />)
|
||||
const clipPath = container.querySelector('clipPath')
|
||||
expect(clipPath).toBeInTheDocument()
|
||||
expect(clipPath).toHaveAttribute('id', 'clip0_2164_11263')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,381 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import Operations from './operations'
|
||||
|
||||
// Mock services
|
||||
vi.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentArchive: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentUnArchive: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentEnable: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentDisable: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentDelete: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentDownload: () => ({ mutateAsync: vi.fn().mockResolvedValue({ url: 'https://example.com/download' }), isPending: false }),
|
||||
useSyncDocument: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useSyncWebsite: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentPause: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
useDocumentResume: () => ({ mutateAsync: vi.fn().mockResolvedValue({}) }),
|
||||
}))
|
||||
|
||||
// Mock utils
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadUrl: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock router
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Operations', () => {
|
||||
const defaultDetail = {
|
||||
name: 'Test Document',
|
||||
enabled: true,
|
||||
archived: false,
|
||||
id: 'doc-123',
|
||||
data_source_type: DataSourceType.FILE,
|
||||
doc_form: 'text',
|
||||
display_status: 'available',
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
embeddingAvailable: true,
|
||||
detail: defaultDetail,
|
||||
datasetId: 'dataset-456',
|
||||
onUpdate: vi.fn(),
|
||||
scene: 'list' as const,
|
||||
className: '',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<Operations {...defaultProps} />)
|
||||
// Should render at least the container
|
||||
expect(document.querySelector('.flex.items-center')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render switch in list scene', () => {
|
||||
const { container } = render(<Operations {...defaultProps} scene="list" />)
|
||||
// Switch component should be rendered
|
||||
const switchEl = container.querySelector('[role="switch"]')
|
||||
expect(switchEl).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render settings button when embedding is available', () => {
|
||||
const { container } = render(<Operations {...defaultProps} />)
|
||||
// Settings button has RiEqualizer2Line icon inside
|
||||
const settingsButton = container.querySelector('button.mr-2.cursor-pointer')
|
||||
expect(settingsButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Switch Behavior', () => {
|
||||
it('should render enabled switch when document is enabled', () => {
|
||||
const { container } = render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, enabled: true, archived: false }}
|
||||
/>,
|
||||
)
|
||||
const switchEl = container.querySelector('[role="switch"]')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should render disabled switch when document is disabled', () => {
|
||||
const { container } = render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, enabled: false, archived: false }}
|
||||
/>,
|
||||
)
|
||||
const switchEl = container.querySelector('[role="switch"]')
|
||||
expect(switchEl).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should show tooltip and disable switch when document is archived', () => {
|
||||
const { container } = render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, archived: true }}
|
||||
/>,
|
||||
)
|
||||
const switchEl = container.querySelector('[role="switch"]')
|
||||
// Archived documents have visually disabled switch (CSS-based)
|
||||
expect(switchEl).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Embedding Not Available', () => {
|
||||
it('should show disabled switch when embedding not available in list scene', () => {
|
||||
const { container } = render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
embeddingAvailable={false}
|
||||
scene="list"
|
||||
/>,
|
||||
)
|
||||
const switchEl = container.querySelector('[role="switch"]')
|
||||
// Switch is visually disabled (CSS-based)
|
||||
expect(switchEl).toHaveClass('!cursor-not-allowed', '!opacity-50')
|
||||
})
|
||||
|
||||
it('should not show settings or popover when embedding not available', () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
embeddingAvailable={false}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: /settings/i })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('More Actions Popover', () => {
|
||||
it('should show rename option for non-archived documents', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, archived: false }}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Click on the more actions button
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
expect(moreButton).toBeInTheDocument()
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.table\.rename/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show download option for FILE type documents', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, data_source_type: DataSourceType.FILE }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.download/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show sync option for notion documents', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, data_source_type: 'notion_import' }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.sync/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show sync option for web documents', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, data_source_type: DataSourceType.WEB }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.sync/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show archive option for non-archived documents', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, archived: false }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.archive/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show unarchive option for archived documents', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, archived: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.unarchive/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show delete option', async () => {
|
||||
render(<Operations {...defaultProps} />)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.delete/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show pause option when status is indexing', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, display_status: 'indexing', archived: false }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.pause/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show resume option when status is paused', async () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{ ...defaultDetail, display_status: 'paused', archived: false }}
|
||||
/>,
|
||||
)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.action\.resume/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete Confirmation Modal', () => {
|
||||
it('should show delete confirmation modal when delete is clicked', async () => {
|
||||
render(<Operations {...defaultProps} />)
|
||||
|
||||
const moreButton = document.querySelector('[class*="commonIcon"]')
|
||||
if (moreButton)
|
||||
fireEvent.click(moreButton)
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteButton = screen.getByText(/list\.action\.delete/i)
|
||||
fireEvent.click(deleteButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/list\.delete\.title/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/list\.delete\.content/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Scene Variations', () => {
|
||||
it('should render correctly in detail scene', () => {
|
||||
render(<Operations {...defaultProps} scene="detail" />)
|
||||
// Settings button should still be visible
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should apply different styles in detail scene', () => {
|
||||
const { container } = render(<Operations {...defaultProps} scene="detail" />)
|
||||
// The component should render without the list-specific styles
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined detail properties', () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
detail={{
|
||||
name: '',
|
||||
enabled: false,
|
||||
archived: false,
|
||||
id: '',
|
||||
data_source_type: '',
|
||||
doc_form: '',
|
||||
display_status: undefined,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
// Should not crash
|
||||
expect(document.querySelector('.flex.items-center')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should stop event propagation on click', () => {
|
||||
const parentHandler = vi.fn()
|
||||
render(
|
||||
<div onClick={parentHandler}>
|
||||
<Operations {...defaultProps} />
|
||||
</div>,
|
||||
)
|
||||
|
||||
const container = document.querySelector('.flex.items-center')
|
||||
if (container)
|
||||
fireEvent.click(container)
|
||||
|
||||
// Parent handler should not be called due to stopPropagation
|
||||
expect(parentHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle custom className', () => {
|
||||
render(<Operations {...defaultProps} className="custom-class" />)
|
||||
// Component should render with the custom class
|
||||
expect(document.querySelector('.flex.items-center')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Selected IDs Handling', () => {
|
||||
it('should pass selectedIds to operations', () => {
|
||||
render(
|
||||
<Operations
|
||||
{...defaultProps}
|
||||
selectedIds={['doc-123', 'doc-456']}
|
||||
onSelectedIdChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
// Component should render correctly with selectedIds
|
||||
expect(document.querySelector('.flex.items-center')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,183 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
// Import after mock
|
||||
import { renameDocumentName } from '@/service/datasets'
|
||||
|
||||
import RenameModal from './rename-modal'
|
||||
|
||||
// Mock the service
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
renameDocumentName: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockRenameDocumentName = vi.mocked(renameDocumentName)
|
||||
|
||||
describe('RenameModal', () => {
|
||||
const defaultProps = {
|
||||
datasetId: 'dataset-123',
|
||||
documentId: 'doc-456',
|
||||
name: 'Original Document',
|
||||
onClose: vi.fn(),
|
||||
onSaved: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.table\.rename/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal title', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.table\.rename/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render name label', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
expect(screen.getByText(/list\.table\.name/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render input with initial name', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveValue('Original Document')
|
||||
})
|
||||
|
||||
it('should render cancel button', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
expect(screen.getByText(/operation\.cancel/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render save button', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
expect(screen.getByText(/operation\.save/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should display the provided name in input', () => {
|
||||
render(<RenameModal {...defaultProps} name="Custom Name" />)
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveValue('Custom Name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should update input value when typing', () => {
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'New Name' } })
|
||||
|
||||
expect(input).toHaveValue('New Name')
|
||||
})
|
||||
|
||||
it('should call onClose when cancel button is clicked', () => {
|
||||
const handleClose = vi.fn()
|
||||
render(<RenameModal {...defaultProps} onClose={handleClose} />)
|
||||
|
||||
const cancelButton = screen.getByText(/operation\.cancel/i)
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call renameDocumentName with correct params when save is clicked', async () => {
|
||||
mockRenameDocumentName.mockResolvedValueOnce({ result: 'success' })
|
||||
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'New Document Name' } })
|
||||
|
||||
const saveButton = screen.getByText(/operation\.save/i)
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRenameDocumentName).toHaveBeenCalledWith({
|
||||
datasetId: 'dataset-123',
|
||||
documentId: 'doc-456',
|
||||
name: 'New Document Name',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onSaved and onClose on successful save', async () => {
|
||||
mockRenameDocumentName.mockResolvedValueOnce({ result: 'success' })
|
||||
const handleSaved = vi.fn()
|
||||
const handleClose = vi.fn()
|
||||
|
||||
render(<RenameModal {...defaultProps} onSaved={handleSaved} onClose={handleClose} />)
|
||||
|
||||
const saveButton = screen.getByText(/operation\.save/i)
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSaved).toHaveBeenCalledTimes(1)
|
||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should show loading state while saving', async () => {
|
||||
// Create a promise that we can resolve manually
|
||||
let resolvePromise: (value: { result: 'success' | 'fail' }) => void
|
||||
const pendingPromise = new Promise<{ result: 'success' | 'fail' }>((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
mockRenameDocumentName.mockReturnValueOnce(pendingPromise)
|
||||
|
||||
render(<RenameModal {...defaultProps} />)
|
||||
const saveButton = screen.getByText(/operation\.save/i)
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
// The button should be in loading state
|
||||
await waitFor(() => {
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const saveBtn = buttons.find(btn => btn.textContent?.includes('operation.save'))
|
||||
expect(saveBtn).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Resolve the promise to clean up
|
||||
resolvePromise!({ result: 'success' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle API error gracefully', async () => {
|
||||
const error = new Error('API Error')
|
||||
mockRenameDocumentName.mockRejectedValueOnce(error)
|
||||
const handleSaved = vi.fn()
|
||||
const handleClose = vi.fn()
|
||||
|
||||
render(<RenameModal {...defaultProps} onSaved={handleSaved} onClose={handleClose} />)
|
||||
|
||||
const saveButton = screen.getByText(/operation\.save/i)
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
// onSaved and onClose should not be called on error
|
||||
expect(handleSaved).not.toHaveBeenCalled()
|
||||
expect(handleClose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty name', () => {
|
||||
render(<RenameModal {...defaultProps} name="" />)
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveValue('')
|
||||
})
|
||||
|
||||
it('should handle name with special characters', () => {
|
||||
render(<RenameModal {...defaultProps} name="Document <with> 'special' chars" />)
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveValue('Document <with> \'special\' chars')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,279 @@
|
||||
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse, FileItem } from '@/models/datasets'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DatasourceType } from '@/models/pipeline'
|
||||
import { StepOnePreview, StepTwoPreview } from './preview-panel'
|
||||
|
||||
// Mock context hooks (底层依赖)
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: vi.fn((selector: (state: unknown) => unknown) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'mock-dataset-id',
|
||||
doc_form: 'text_model',
|
||||
pipeline_id: 'mock-pipeline-id',
|
||||
},
|
||||
}
|
||||
return selector(mockState)
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock API hooks (底层依赖)
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useFilePreview: vi.fn(() => ({
|
||||
data: { content: 'Mock file content for testing' },
|
||||
isFetching: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
usePreviewOnlineDocument: vi.fn(() => ({
|
||||
mutateAsync: vi.fn().mockResolvedValue({ content: 'Mock document content' }),
|
||||
isPending: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock data source store
|
||||
vi.mock('../data-source/store', () => ({
|
||||
useDataSourceStore: vi.fn(() => ({
|
||||
getState: () => ({ currentCredentialId: 'mock-credential-id' }),
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('StepOnePreview', () => {
|
||||
const mockDatasource: Datasource = {
|
||||
nodeId: 'test-node-id',
|
||||
nodeData: { type: 'data-source' } as unknown as DataSourceNodeType,
|
||||
}
|
||||
|
||||
const mockLocalFile: CustomFile = {
|
||||
id: 'file-1',
|
||||
name: 'test-file.txt',
|
||||
type: 'text/plain',
|
||||
size: 1024,
|
||||
progress: 100,
|
||||
extension: 'txt',
|
||||
} as unknown as CustomFile
|
||||
|
||||
const mockWebsite: CrawlResultItem = {
|
||||
source_url: 'https://example.com',
|
||||
title: 'Example Site',
|
||||
markdown: 'Mock markdown content',
|
||||
} as CrawlResultItem
|
||||
|
||||
const defaultProps = {
|
||||
datasource: mockDatasource,
|
||||
currentLocalFile: undefined,
|
||||
currentDocument: undefined,
|
||||
currentWebsite: undefined,
|
||||
hidePreviewLocalFile: vi.fn(),
|
||||
hidePreviewOnlineDocument: vi.fn(),
|
||||
hideWebsitePreview: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<StepOnePreview {...defaultProps} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render container with correct structure', () => {
|
||||
const { container } = render(<StepOnePreview {...defaultProps} />)
|
||||
expect(container.querySelector('.flex.h-full.flex-col')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering - FilePreview', () => {
|
||||
it('should render FilePreview when currentLocalFile is provided', () => {
|
||||
render(<StepOnePreview {...defaultProps} currentLocalFile={mockLocalFile} />)
|
||||
// FilePreview renders a preview header with file name
|
||||
expect(screen.getByText(/test-file/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render FilePreview when currentLocalFile is undefined', () => {
|
||||
const { container } = render(<StepOnePreview {...defaultProps} currentLocalFile={undefined} />)
|
||||
// Container should still render but without file preview content
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering - WebsitePreview', () => {
|
||||
it('should render WebsitePreview when currentWebsite is provided', () => {
|
||||
render(<StepOnePreview {...defaultProps} currentWebsite={mockWebsite} />)
|
||||
// WebsitePreview displays the website title and URL
|
||||
expect(screen.getByText('Example Site')).toBeInTheDocument()
|
||||
expect(screen.getByText('https://example.com')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render WebsitePreview when currentWebsite is undefined', () => {
|
||||
const { container } = render(<StepOnePreview {...defaultProps} currentWebsite={undefined} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call hideWebsitePreview when close button is clicked', () => {
|
||||
const hideWebsitePreview = vi.fn()
|
||||
render(
|
||||
<StepOnePreview
|
||||
{...defaultProps}
|
||||
currentWebsite={mockWebsite}
|
||||
hideWebsitePreview={hideWebsitePreview}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Find and click the close button (RiCloseLine icon)
|
||||
const closeButton = screen.getByRole('button')
|
||||
closeButton.click()
|
||||
|
||||
expect(hideWebsitePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle website with long markdown content', () => {
|
||||
const longWebsite: CrawlResultItem = {
|
||||
...mockWebsite,
|
||||
markdown: 'A'.repeat(10000),
|
||||
}
|
||||
render(<StepOnePreview {...defaultProps} currentWebsite={longWebsite} />)
|
||||
expect(screen.getByText('Example Site')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('StepTwoPreview', () => {
|
||||
const mockFileList: FileItem[] = [
|
||||
{
|
||||
file: {
|
||||
id: 'file-1',
|
||||
name: 'file1.txt',
|
||||
extension: 'txt',
|
||||
size: 1024,
|
||||
} as CustomFile,
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
file: {
|
||||
id: 'file-2',
|
||||
name: 'file2.txt',
|
||||
extension: 'txt',
|
||||
size: 2048,
|
||||
} as CustomFile,
|
||||
progress: 100,
|
||||
},
|
||||
] as FileItem[]
|
||||
|
||||
const mockOnlineDocuments: (NotionPage & { workspace_id: string })[] = [
|
||||
{
|
||||
page_id: 'page-1',
|
||||
page_name: 'Page 1',
|
||||
type: 'page',
|
||||
workspace_id: 'workspace-1',
|
||||
page_icon: null,
|
||||
is_bound: false,
|
||||
parent_id: '',
|
||||
},
|
||||
]
|
||||
|
||||
const mockWebsitePages: CrawlResultItem[] = [
|
||||
{ source_url: 'https://example.com', title: 'Example', markdown: 'Content' } as CrawlResultItem,
|
||||
]
|
||||
|
||||
const mockOnlineDriveFiles: OnlineDriveFile[] = [
|
||||
{ id: 'drive-1', name: 'drive-file.txt' } as OnlineDriveFile,
|
||||
]
|
||||
|
||||
const mockEstimateData: FileIndexingEstimateResponse = {
|
||||
tokens: 1000,
|
||||
total_price: 0.01,
|
||||
total_segments: 10,
|
||||
} as FileIndexingEstimateResponse
|
||||
|
||||
const defaultProps = {
|
||||
datasourceType: DatasourceType.localFile,
|
||||
localFileList: mockFileList,
|
||||
onlineDocuments: mockOnlineDocuments,
|
||||
websitePages: mockWebsitePages,
|
||||
selectedOnlineDriveFileList: mockOnlineDriveFiles,
|
||||
isIdle: true,
|
||||
isPendingPreview: false,
|
||||
estimateData: mockEstimateData,
|
||||
onPreview: vi.fn(),
|
||||
handlePreviewFileChange: vi.fn(),
|
||||
handlePreviewOnlineDocumentChange: vi.fn(),
|
||||
handlePreviewWebsitePageChange: vi.fn(),
|
||||
handlePreviewOnlineDriveFileChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ChunkPreview component structure', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} />)
|
||||
expect(container.querySelector('.flex.h-full.flex-col')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props Passing', () => {
|
||||
it('should render preview button when isIdle is true', () => {
|
||||
render(<StepTwoPreview {...defaultProps} isIdle={true} />)
|
||||
// ChunkPreview shows a preview button when idle
|
||||
const previewButton = screen.queryByRole('button')
|
||||
expect(previewButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
const onPreview = vi.fn()
|
||||
render(<StepTwoPreview {...defaultProps} isIdle={true} onPreview={onPreview} />)
|
||||
|
||||
// Find and click the preview button
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const previewButton = buttons.find(btn => btn.textContent?.toLowerCase().includes('preview'))
|
||||
if (previewButton) {
|
||||
previewButton.click()
|
||||
expect(onPreview).toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty localFileList', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} localFileList={[]} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty onlineDocuments', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} onlineDocuments={[]} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty websitePages', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} websitePages={[]} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty onlineDriveFiles', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} selectedOnlineDriveFileList={[]} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined estimateData', () => {
|
||||
const { container } = render(<StepTwoPreview {...defaultProps} estimateData={undefined} />)
|
||||
expect(container.querySelector('.h-full')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,413 @@
|
||||
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DatasourceType } from '@/models/pipeline'
|
||||
import StepOneContent from './step-one-content'
|
||||
|
||||
// Mock context providers and hooks (底层依赖)
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(() => ({
|
||||
setShowPricingModal: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock billing components that have complex provider dependencies
|
||||
vi.mock('@/app/components/billing/vector-space-full', () => ({
|
||||
default: () => <div data-testid="vector-space-full">Vector Space Full</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/billing/upgrade-btn', () => ({
|
||||
default: ({ onClick }: { onClick?: () => void }) => (
|
||||
<button data-testid="upgrade-btn" onClick={onClick}>Upgrade</button>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock data source store
|
||||
vi.mock('../data-source/store', () => ({
|
||||
useDataSourceStore: vi.fn(() => ({
|
||||
getState: () => ({
|
||||
localFileList: [],
|
||||
currentCredentialId: 'mock-credential-id',
|
||||
}),
|
||||
setState: vi.fn(),
|
||||
})),
|
||||
useDataSourceStoreWithSelector: vi.fn((selector: (state: unknown) => unknown) => {
|
||||
const mockState = {
|
||||
localFileList: [],
|
||||
onlineDocuments: [],
|
||||
websitePages: [],
|
||||
selectedOnlineDriveFileList: [],
|
||||
}
|
||||
return selector(mockState)
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock file upload config
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useFileUploadConfig: vi.fn(() => ({
|
||||
data: {
|
||||
file_size_limit: 15 * 1024 * 1024,
|
||||
batch_count_limit: 20,
|
||||
document_file_extensions: ['.txt', '.md', '.pdf'],
|
||||
},
|
||||
isLoading: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock hooks used by data source options
|
||||
vi.mock('../hooks', () => ({
|
||||
useDatasourceOptions: vi.fn(() => [
|
||||
{ label: 'Local File', value: 'node-1', data: { type: 'data-source' } },
|
||||
]),
|
||||
}))
|
||||
|
||||
// Mock useDatasourceIcon hook to avoid complex data source list transformation
|
||||
vi.mock('../data-source-options/hooks', () => ({
|
||||
useDatasourceIcon: vi.fn(() => '/icons/local-file.svg'),
|
||||
}))
|
||||
|
||||
// Mock the entire local-file component since it has deep context dependencies
|
||||
vi.mock('../data-source/local-file', () => ({
|
||||
default: ({ allowedExtensions, supportBatchUpload }: {
|
||||
allowedExtensions: string[]
|
||||
supportBatchUpload: boolean
|
||||
}) => (
|
||||
<div data-testid="local-file">
|
||||
<div>Drag and drop file here</div>
|
||||
<span data-testid="allowed-extensions">{allowedExtensions.join(',')}</span>
|
||||
<span data-testid="support-batch-upload">{String(supportBatchUpload)}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock online documents since it has complex OAuth/API dependencies
|
||||
vi.mock('../data-source/online-documents', () => ({
|
||||
default: ({ nodeId, onCredentialChange }: {
|
||||
nodeId: string
|
||||
onCredentialChange: (credentialId: string) => void
|
||||
}) => (
|
||||
<div data-testid="online-documents">
|
||||
<span data-testid="online-doc-node-id">{nodeId}</span>
|
||||
<button data-testid="credential-change-btn" onClick={() => onCredentialChange('new-credential')}>
|
||||
Change Credential
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock website crawl
|
||||
vi.mock('../data-source/website-crawl', () => ({
|
||||
default: ({ nodeId, onCredentialChange }: {
|
||||
nodeId: string
|
||||
onCredentialChange: (credentialId: string) => void
|
||||
}) => (
|
||||
<div data-testid="website-crawl">
|
||||
<span data-testid="website-crawl-node-id">{nodeId}</span>
|
||||
<button data-testid="website-credential-btn" onClick={() => onCredentialChange('website-credential')}>
|
||||
Change Website Credential
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock online drive
|
||||
vi.mock('../data-source/online-drive', () => ({
|
||||
default: ({ nodeId, onCredentialChange }: {
|
||||
nodeId: string
|
||||
onCredentialChange: (credentialId: string) => void
|
||||
}) => (
|
||||
<div data-testid="online-drive">
|
||||
<span data-testid="online-drive-node-id">{nodeId}</span>
|
||||
<button data-testid="drive-credential-btn" onClick={() => onCredentialChange('drive-credential')}>
|
||||
Change Drive Credential
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock locale context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en'),
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
// Mock theme hook
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: vi.fn(() => 'light'),
|
||||
}))
|
||||
|
||||
// Mock upload service
|
||||
vi.mock('@/service/base', () => ({
|
||||
upload: vi.fn().mockResolvedValue({ id: 'uploaded-file-id' }),
|
||||
}))
|
||||
|
||||
// Mock next/navigation
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'mock-dataset-id' }),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
usePathname: () => '/datasets/mock-dataset-id',
|
||||
}))
|
||||
|
||||
// Mock pipeline service hooks
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useNotionWorkspaces: vi.fn(() => ({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
})),
|
||||
useNotionPages: vi.fn(() => ({
|
||||
data: { pages: [] },
|
||||
isLoading: false,
|
||||
})),
|
||||
useDataSourceList: vi.fn(() => ({
|
||||
data: [
|
||||
{
|
||||
type: 'local_file',
|
||||
declaration: {
|
||||
identity: {
|
||||
name: 'Local File',
|
||||
icon: '/icons/local-file.svg',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
isSuccess: true,
|
||||
isLoading: false,
|
||||
})),
|
||||
useCrawlResult: vi.fn(() => ({
|
||||
data: { data: [] },
|
||||
isLoading: false,
|
||||
})),
|
||||
useSupportedOauth: vi.fn(() => ({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
})),
|
||||
useOnlineDriveCredentialList: vi.fn(() => ({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
})),
|
||||
useOnlineDriveFileList: vi.fn(() => ({
|
||||
data: { data: [] },
|
||||
isLoading: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('StepOneContent', () => {
|
||||
const mockDatasource: Datasource = {
|
||||
nodeId: 'test-node-id',
|
||||
nodeData: {
|
||||
type: 'data-source',
|
||||
fileExtensions: ['txt', 'pdf'],
|
||||
title: 'Test Data Source',
|
||||
desc: 'Test description',
|
||||
} as unknown as DataSourceNodeType,
|
||||
}
|
||||
|
||||
const mockPipelineNodes: Node<DataSourceNodeType>[] = [
|
||||
{
|
||||
id: 'node-1',
|
||||
data: {
|
||||
type: 'data-source',
|
||||
title: 'Node 1',
|
||||
desc: 'Description 1',
|
||||
} as unknown as DataSourceNodeType,
|
||||
} as Node<DataSourceNodeType>,
|
||||
{
|
||||
id: 'node-2',
|
||||
data: {
|
||||
type: 'data-source',
|
||||
title: 'Node 2',
|
||||
desc: 'Description 2',
|
||||
} as unknown as DataSourceNodeType,
|
||||
} as Node<DataSourceNodeType>,
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
datasource: mockDatasource,
|
||||
datasourceType: DatasourceType.localFile,
|
||||
pipelineNodes: mockPipelineNodes,
|
||||
supportBatchUpload: true,
|
||||
localFileListLength: 0,
|
||||
isShowVectorSpaceFull: false,
|
||||
showSelect: false,
|
||||
totalOptions: 10,
|
||||
selectedOptions: 5,
|
||||
tip: 'Test tip',
|
||||
nextBtnDisabled: false,
|
||||
onSelectDataSource: vi.fn(),
|
||||
onCredentialChange: vi.fn(),
|
||||
onSelectAll: vi.fn(),
|
||||
onNextStep: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const { container } = render(<StepOneContent {...defaultProps} />)
|
||||
expect(container.querySelector('.flex.flex-col')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render DataSourceOptions component', () => {
|
||||
render(<StepOneContent {...defaultProps} />)
|
||||
// DataSourceOptions renders option cards
|
||||
expect(screen.getByText('Local File')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Actions component with next button', () => {
|
||||
render(<StepOneContent {...defaultProps} />)
|
||||
// Actions component renders a next step button (uses i18n key)
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation\.stepOne\.button/i })
|
||||
expect(nextButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering - DatasourceType', () => {
|
||||
it('should render LocalFile component when datasourceType is localFile', () => {
|
||||
render(<StepOneContent {...defaultProps} datasourceType={DatasourceType.localFile} />)
|
||||
expect(screen.getByTestId('local-file')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render OnlineDocuments component when datasourceType is onlineDocument', () => {
|
||||
render(<StepOneContent {...defaultProps} datasourceType={DatasourceType.onlineDocument} />)
|
||||
expect(screen.getByTestId('online-documents')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render WebsiteCrawl component when datasourceType is websiteCrawl', () => {
|
||||
render(<StepOneContent {...defaultProps} datasourceType={DatasourceType.websiteCrawl} />)
|
||||
expect(screen.getByTestId('website-crawl')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render OnlineDrive component when datasourceType is onlineDrive', () => {
|
||||
render(<StepOneContent {...defaultProps} datasourceType={DatasourceType.onlineDrive} />)
|
||||
expect(screen.getByTestId('online-drive')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render data source component when datasourceType is undefined', () => {
|
||||
const { container } = render(<StepOneContent {...defaultProps} datasourceType={undefined} />)
|
||||
expect(container.querySelector('.flex.flex-col')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('local-file')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering - VectorSpaceFull', () => {
|
||||
it('should render VectorSpaceFull when isShowVectorSpaceFull is true', () => {
|
||||
render(<StepOneContent {...defaultProps} isShowVectorSpaceFull={true} />)
|
||||
expect(screen.getByTestId('vector-space-full')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render VectorSpaceFull when isShowVectorSpaceFull is false', () => {
|
||||
render(<StepOneContent {...defaultProps} isShowVectorSpaceFull={false} />)
|
||||
expect(screen.queryByTestId('vector-space-full')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conditional Rendering - UpgradeCard', () => {
|
||||
it('should render UpgradeCard when batch upload not supported and has local files', () => {
|
||||
render(
|
||||
<StepOneContent
|
||||
{...defaultProps}
|
||||
supportBatchUpload={false}
|
||||
datasourceType={DatasourceType.localFile}
|
||||
localFileListLength={3}
|
||||
/>,
|
||||
)
|
||||
// UpgradeCard contains an upgrade button
|
||||
expect(screen.getByTestId('upgrade-btn')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render UpgradeCard when batch upload is supported', () => {
|
||||
render(
|
||||
<StepOneContent
|
||||
{...defaultProps}
|
||||
supportBatchUpload={true}
|
||||
datasourceType={DatasourceType.localFile}
|
||||
localFileListLength={3}
|
||||
/>,
|
||||
)
|
||||
// The upgrade card should not be present
|
||||
const upgradeCard = screen.queryByText(/upload multiple files/i)
|
||||
expect(upgradeCard).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render UpgradeCard when datasourceType is not localFile', () => {
|
||||
render(
|
||||
<StepOneContent
|
||||
{...defaultProps}
|
||||
supportBatchUpload={false}
|
||||
datasourceType={undefined}
|
||||
localFileListLength={3}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('upgrade-btn')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render UpgradeCard when localFileListLength is 0', () => {
|
||||
render(
|
||||
<StepOneContent
|
||||
{...defaultProps}
|
||||
supportBatchUpload={false}
|
||||
datasourceType={DatasourceType.localFile}
|
||||
localFileListLength={0}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('upgrade-btn')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onNextStep when next button is clicked', () => {
|
||||
const onNextStep = vi.fn()
|
||||
render(<StepOneContent {...defaultProps} onNextStep={onNextStep} />)
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation\.stepOne\.button/i })
|
||||
nextButton.click()
|
||||
|
||||
expect(onNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should disable next button when nextBtnDisabled is true', () => {
|
||||
render(<StepOneContent {...defaultProps} nextBtnDisabled={true} />)
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation\.stepOne\.button/i })
|
||||
expect(nextButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined datasource when datasourceType is undefined', () => {
|
||||
const { container } = render(
|
||||
<StepOneContent {...defaultProps} datasource={undefined} datasourceType={undefined} />,
|
||||
)
|
||||
expect(container.querySelector('.flex.flex-col')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty pipelineNodes array', () => {
|
||||
render(<StepOneContent {...defaultProps} pipelineNodes={[]} />)
|
||||
// Should still render but DataSourceOptions may show no options
|
||||
const { container } = render(<StepOneContent {...defaultProps} pipelineNodes={[]} />)
|
||||
expect(container.querySelector('.flex.flex-col')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined totalOptions', () => {
|
||||
render(<StepOneContent {...defaultProps} totalOptions={undefined} />)
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation\.stepOne\.button/i })
|
||||
expect(nextButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined selectedOptions', () => {
|
||||
render(<StepOneContent {...defaultProps} selectedOptions={undefined} />)
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation\.stepOne\.button/i })
|
||||
expect(nextButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty tip', () => {
|
||||
render(<StepOneContent {...defaultProps} tip="" />)
|
||||
const nextButton = screen.getByRole('button', { name: /datasetCreation\.stepOne\.button/i })
|
||||
expect(nextButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,97 @@
|
||||
import type { InitialDocumentDetail } from '@/models/pipeline'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import StepThreeContent from './step-three-content'
|
||||
|
||||
// Mock context hooks used by Processing component
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: vi.fn((selector: (state: unknown) => unknown) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'mock-dataset-id',
|
||||
indexing_technique: 'high_quality',
|
||||
retrieval_model_dict: {
|
||||
search_method: 'semantic_search',
|
||||
},
|
||||
},
|
||||
}
|
||||
return selector(mockState)
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
// Mock EmbeddingProcess component as it has complex dependencies
|
||||
vi.mock('../processing/embedding-process', () => ({
|
||||
default: ({ datasetId, batchId, documents }: {
|
||||
datasetId: string
|
||||
batchId: string
|
||||
documents: InitialDocumentDetail[]
|
||||
}) => (
|
||||
<div data-testid="embedding-process">
|
||||
<span data-testid="dataset-id">{datasetId}</span>
|
||||
<span data-testid="batch-id">{batchId}</span>
|
||||
<span data-testid="documents-count">{documents.length}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('StepThreeContent', () => {
|
||||
const mockDocuments: InitialDocumentDetail[] = [
|
||||
{ id: 'doc1', name: 'Document 1' } as InitialDocumentDetail,
|
||||
{ id: 'doc2', name: 'Document 2' } as InitialDocumentDetail,
|
||||
]
|
||||
|
||||
const defaultProps = {
|
||||
batchId: 'test-batch-id',
|
||||
documents: mockDocuments,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<StepThreeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Processing component', () => {
|
||||
render(<StepThreeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should pass batchId to Processing component', () => {
|
||||
render(<StepThreeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('batch-id')).toHaveTextContent('test-batch-id')
|
||||
})
|
||||
|
||||
it('should pass documents to Processing component', () => {
|
||||
render(<StepThreeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('documents-count')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('should handle empty documents array', () => {
|
||||
render(<StepThreeContent batchId="test-batch-id" documents={[]} />)
|
||||
expect(screen.getByTestId('documents-count')).toHaveTextContent('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should render with different batchId', () => {
|
||||
render(<StepThreeContent batchId="another-batch-id" documents={mockDocuments} />)
|
||||
expect(screen.getByTestId('batch-id')).toHaveTextContent('another-batch-id')
|
||||
})
|
||||
|
||||
it('should render with single document', () => {
|
||||
const singleDocument = [mockDocuments[0]]
|
||||
render(<StepThreeContent batchId="test-batch-id" documents={singleDocument} />)
|
||||
expect(screen.getByTestId('documents-count')).toHaveTextContent('1')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,136 @@
|
||||
import type { RefObject } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import StepTwoContent from './step-two-content'
|
||||
|
||||
// Mock ProcessDocuments component as it has complex hook dependencies
|
||||
vi.mock('../process-documents', () => ({
|
||||
default: vi.fn().mockImplementation(({
|
||||
dataSourceNodeId,
|
||||
isRunning,
|
||||
onProcess,
|
||||
onPreview,
|
||||
onSubmit,
|
||||
onBack,
|
||||
}: {
|
||||
dataSourceNodeId: string
|
||||
isRunning: boolean
|
||||
onProcess: () => void
|
||||
onPreview: () => void
|
||||
onSubmit: (data: Record<string, unknown>) => void
|
||||
onBack: () => void
|
||||
}) => (
|
||||
<div data-testid="process-documents">
|
||||
<span data-testid="data-source-node-id">{dataSourceNodeId}</span>
|
||||
<span data-testid="is-running">{String(isRunning)}</span>
|
||||
<button data-testid="process-btn" onClick={onProcess}>Process</button>
|
||||
<button data-testid="preview-btn" onClick={onPreview}>Preview</button>
|
||||
<button data-testid="submit-btn" onClick={() => onSubmit({ key: 'value' })}>Submit</button>
|
||||
<button data-testid="back-btn" onClick={onBack}>Back</button>
|
||||
</div>
|
||||
)),
|
||||
}))
|
||||
|
||||
describe('StepTwoContent', () => {
|
||||
const mockFormRef: RefObject<{ submit: () => void } | null> = { current: null }
|
||||
|
||||
const defaultProps = {
|
||||
formRef: mockFormRef,
|
||||
dataSourceNodeId: 'test-node-id',
|
||||
isRunning: false,
|
||||
onProcess: vi.fn(),
|
||||
onPreview: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
onBack: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<StepTwoContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('process-documents')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ProcessDocuments component', () => {
|
||||
render(<StepTwoContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('process-documents')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should pass dataSourceNodeId to ProcessDocuments', () => {
|
||||
render(<StepTwoContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('data-source-node-id')).toHaveTextContent('test-node-id')
|
||||
})
|
||||
|
||||
it('should pass isRunning false to ProcessDocuments', () => {
|
||||
render(<StepTwoContent {...defaultProps} isRunning={false} />)
|
||||
expect(screen.getByTestId('is-running')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should pass isRunning true to ProcessDocuments', () => {
|
||||
render(<StepTwoContent {...defaultProps} isRunning={true} />)
|
||||
expect(screen.getByTestId('is-running')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should pass different dataSourceNodeId', () => {
|
||||
render(<StepTwoContent {...defaultProps} dataSourceNodeId="different-node-id" />)
|
||||
expect(screen.getByTestId('data-source-node-id')).toHaveTextContent('different-node-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onProcess when process button is clicked', () => {
|
||||
const onProcess = vi.fn()
|
||||
render(<StepTwoContent {...defaultProps} onProcess={onProcess} />)
|
||||
|
||||
screen.getByTestId('process-btn').click()
|
||||
|
||||
expect(onProcess).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
const onPreview = vi.fn()
|
||||
render(<StepTwoContent {...defaultProps} onPreview={onPreview} />)
|
||||
|
||||
screen.getByTestId('preview-btn').click()
|
||||
|
||||
expect(onPreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSubmit when submit button is clicked', () => {
|
||||
const onSubmit = vi.fn()
|
||||
render(<StepTwoContent {...defaultProps} onSubmit={onSubmit} />)
|
||||
|
||||
screen.getByTestId('submit-btn').click()
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1)
|
||||
expect(onSubmit).toHaveBeenCalledWith({ key: 'value' })
|
||||
})
|
||||
|
||||
it('should call onBack when back button is clicked', () => {
|
||||
const onBack = vi.fn()
|
||||
render(<StepTwoContent {...defaultProps} onBack={onBack} />)
|
||||
|
||||
screen.getByTestId('back-btn').click()
|
||||
|
||||
expect(onBack).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty dataSourceNodeId', () => {
|
||||
render(<StepTwoContent {...defaultProps} dataSourceNodeId="" />)
|
||||
expect(screen.getByTestId('data-source-node-id')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle null formRef', () => {
|
||||
const nullRef = { current: null }
|
||||
render(<StepTwoContent {...defaultProps} formRef={nullRef} />)
|
||||
expect(screen.getByTestId('process-documents')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,243 @@
|
||||
import type { ReactNode } from 'react'
|
||||
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: 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(['问题', '答案'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,485 @@
|
||||
import type { ReactNode } from 'react'
|
||||
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: ReactNode }) => children,
|
||||
Consumer: ({ children }: { children: (ctx: { notify: typeof mockNotify }) => 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',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle file without extension', () => {
|
||||
// Arrange
|
||||
const { container } = render(<CSVUploader {...defaultProps} />)
|
||||
const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement
|
||||
const testFile = new File(['content'], 'noextension', { type: 'text/plain' })
|
||||
|
||||
// Act
|
||||
fireEvent.change(fileInput, { target: { files: [testFile] } })
|
||||
|
||||
// Assert
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Drag and drop tests
|
||||
// Note: Native drag and drop events use addEventListener which is set up in useEffect.
|
||||
// Testing these requires triggering native DOM events on the actual dropRef element.
|
||||
describe('Drag and Drop', () => {
|
||||
it('should render drop zone element', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<CSVUploader {...defaultProps} />)
|
||||
|
||||
// Assert - drop zone should exist for drag and drop
|
||||
const dropZone = container.querySelector('div > div')
|
||||
expect(dropZone).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have drag overlay element that can appear during drag', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<CSVUploader {...defaultProps} />)
|
||||
|
||||
// Assert - component structure supports dragging
|
||||
expect(container.querySelector('div')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Upload progress callback tests
|
||||
describe('Upload Progress Callbacks', () => {
|
||||
it('should update progress during file upload', async () => {
|
||||
// Arrange
|
||||
const mockUpdateFile = vi.fn()
|
||||
let progressCallback: ((e: ProgressEvent) => void) | undefined
|
||||
|
||||
mockUpload.mockImplementation(({ onprogress }) => {
|
||||
progressCallback = onprogress
|
||||
return Promise.resolve({ 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] } })
|
||||
|
||||
// Simulate progress event
|
||||
if (progressCallback) {
|
||||
const progressEvent = new ProgressEvent('progress', {
|
||||
lengthComputable: true,
|
||||
loaded: 50,
|
||||
total: 100,
|
||||
})
|
||||
progressCallback(progressEvent)
|
||||
}
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
progress: expect.any(Number),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle progress event with lengthComputable false', async () => {
|
||||
// Arrange
|
||||
const mockUpdateFile = vi.fn()
|
||||
let progressCallback: ((e: ProgressEvent) => void) | undefined
|
||||
|
||||
mockUpload.mockImplementation(({ onprogress }) => {
|
||||
progressCallback = onprogress
|
||||
return Promise.resolve({ 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] } })
|
||||
|
||||
// Simulate progress event with lengthComputable false
|
||||
if (progressCallback) {
|
||||
const progressEvent = new ProgressEvent('progress', {
|
||||
lengthComputable: false,
|
||||
loaded: 50,
|
||||
total: 100,
|
||||
})
|
||||
progressCallback(progressEvent)
|
||||
}
|
||||
|
||||
// Assert - should complete upload without progress updates when lengthComputable is false
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateFile).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,232 @@
|
||||
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()
|
||||
})
|
||||
|
||||
it('should handle file cleared after upload', async () => {
|
||||
// Arrange
|
||||
const mockOnConfirm = vi.fn()
|
||||
render(<BatchModal {...defaultProps} onConfirm={mockOnConfirm} />)
|
||||
|
||||
// Upload a file first
|
||||
fireEvent.click(screen.getByTestId('upload-btn'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('file-info')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Clear the file
|
||||
fireEvent.click(screen.getByTestId('clear-btn'))
|
||||
|
||||
// Assert - run button should be disabled again
|
||||
const runButton = screen.getByText(/list\.batchModal\.run/i).closest('button')
|
||||
expect(runButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,330 @@
|
||||
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
|
||||
let mockSubscriptionCallback: ((v: string) => void) | null = null
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
eventEmitter: {
|
||||
useSubscription: (callback: (v: string) => void) => {
|
||||
mockSubscriptionCallback = callback
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// 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
|
||||
mockSubscriptionCallback = null
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
// Event subscription tests
|
||||
describe('Event Subscription', () => {
|
||||
it('should register event subscription', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentDetail {...defaultProps} />)
|
||||
|
||||
// Assert - subscription callback should be registered
|
||||
expect(mockSubscriptionCallback).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should have save button enabled by default', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentDetail {...defaultProps} />)
|
||||
|
||||
// Assert - save button should be enabled initially
|
||||
expect(screen.getByTestId('save-btn')).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// Cancel behavior
|
||||
describe('Cancel Behavior', () => {
|
||||
it('should call onCancel when cancel button is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnCancel = vi.fn()
|
||||
render(<ChildSegmentDetail {...defaultProps} onCancel={mockOnCancel} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,499 +1,430 @@
|
||||
import type { DocumentContextValue } from '@/app/components/datasets/documents/detail/context'
|
||||
import type { ChildChunkDetail, ChunkingMode, ParentMode } from '@/models/datasets'
|
||||
import type { ChildChunkDetail } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import ChildSegmentList from './child-segment-list'
|
||||
|
||||
// ============================================================================
|
||||
// Hoisted Mocks
|
||||
// ============================================================================
|
||||
|
||||
const {
|
||||
mockParentMode,
|
||||
mockCurrChildChunk,
|
||||
} = vi.hoisted(() => ({
|
||||
mockParentMode: { current: 'paragraph' as ParentMode },
|
||||
mockCurrChildChunk: { current: { childChunkInfo: undefined, showModal: false } as { childChunkInfo?: ChildChunkDetail, showModal: boolean } },
|
||||
}))
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { count?: number, ns?: string }) => {
|
||||
if (key === 'segment.childChunks')
|
||||
return options?.count === 1 ? 'child chunk' : 'child chunks'
|
||||
if (key === 'segment.searchResults')
|
||||
return 'search results'
|
||||
if (key === 'segment.edited')
|
||||
return 'edited'
|
||||
if (key === 'operation.add')
|
||||
return 'Add'
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock document context
|
||||
let mockParentMode = 'paragraph'
|
||||
vi.mock('../context', () => ({
|
||||
useDocumentContext: (selector: (value: DocumentContextValue) => unknown) => {
|
||||
const value: DocumentContextValue = {
|
||||
datasetId: 'test-dataset-id',
|
||||
documentId: 'test-document-id',
|
||||
docForm: 'text' as ChunkingMode,
|
||||
parentMode: mockParentMode.current,
|
||||
}
|
||||
return selector(value)
|
||||
useDocumentContext: (selector: (state: { parentMode: string }) => unknown) => {
|
||||
return selector({ parentMode: mockParentMode })
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock segment list context
|
||||
let mockCurrChildChunk: { childChunkInfo: { id: string } } | null = null
|
||||
vi.mock('./index', () => ({
|
||||
useSegmentListContext: (selector: (value: { currChildChunk: { childChunkInfo?: ChildChunkDetail, showModal: boolean } }) => unknown) => {
|
||||
return selector({ currChildChunk: mockCurrChildChunk.current })
|
||||
useSegmentListContext: (selector: (state: { currChildChunk: { childChunkInfo: { id: string } } | null }) => unknown) => {
|
||||
return selector({ currChildChunk: mockCurrChildChunk })
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock skeleton component
|
||||
vi.mock('./skeleton/full-doc-list-skeleton', () => ({
|
||||
default: () => <div data-testid="full-doc-list-skeleton">Loading...</div>,
|
||||
}))
|
||||
|
||||
// Mock Empty component
|
||||
// Mock child components
|
||||
vi.mock('./common/empty', () => ({
|
||||
default: ({ onClearFilter }: { onClearFilter: () => void }) => (
|
||||
<div data-testid="empty-component">
|
||||
<button onClick={onClearFilter}>Clear Filter</button>
|
||||
<div data-testid="empty">
|
||||
<button onClick={onClearFilter} data-testid="clear-filter-btn">Clear Filter</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./skeleton/full-doc-list-skeleton', () => ({
|
||||
default: () => <div data-testid="full-doc-skeleton">Loading...</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../formatted-text/flavours/edit-slice', () => ({
|
||||
EditSlice: ({
|
||||
label,
|
||||
text,
|
||||
onDelete,
|
||||
className,
|
||||
labelClassName,
|
||||
onClick,
|
||||
}: {
|
||||
label: string
|
||||
text: string
|
||||
onDelete: () => void
|
||||
className: string
|
||||
labelClassName: string
|
||||
contentClassName: string
|
||||
labelInnerClassName: string
|
||||
showDivider: boolean
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
offsetOptions: unknown
|
||||
}) => (
|
||||
<div data-testid="edit-slice" className={className}>
|
||||
<span data-testid="slice-label" className={labelClassName}>{label}</span>
|
||||
<span data-testid="slice-text">{text}</span>
|
||||
<button data-testid="delete-slice-btn" onClick={onDelete}>Delete</button>
|
||||
<button data-testid="click-slice-btn" onClick={e => onClick(e)}>Click</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock FormattedText and EditSlice
|
||||
vi.mock('../../../formatted-text/formatted', () => ({
|
||||
FormattedText: ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||
FormattedText: ({ children, className }: { children: React.ReactNode, className: string }) => (
|
||||
<div data-testid="formatted-text" className={className}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../formatted-text/flavours/edit-slice', () => ({
|
||||
EditSlice: ({ label, text, onDelete, onClick, labelClassName, contentClassName }: {
|
||||
label: string
|
||||
text: string
|
||||
onDelete: () => void
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
labelClassName?: string
|
||||
contentClassName?: string
|
||||
}) => (
|
||||
<div data-testid="edit-slice" onClick={onClick}>
|
||||
<span data-testid="edit-slice-label" className={labelClassName}>{label}</span>
|
||||
<span data-testid="edit-slice-content" className={contentClassName}>{text}</span>
|
||||
<button
|
||||
data-testid="delete-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete()
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
const createMockChildChunk = (overrides: Partial<ChildChunkDetail> = {}): ChildChunkDetail => ({
|
||||
id: `child-${Math.random().toString(36).substr(2, 9)}`,
|
||||
position: 1,
|
||||
segment_id: 'segment-1',
|
||||
content: 'Child chunk content',
|
||||
word_count: 100,
|
||||
created_at: 1700000000,
|
||||
updated_at: 1700000000,
|
||||
type: 'automatic',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('ChildSegmentList', () => {
|
||||
const defaultProps = {
|
||||
childChunks: [] as ChildChunkDetail[],
|
||||
parentChunkId: 'parent-1',
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockParentMode.current = 'paragraph'
|
||||
mockCurrChildChunk.current = { childChunkInfo: undefined, showModal: false }
|
||||
mockParentMode = 'paragraph'
|
||||
mockCurrChildChunk = null
|
||||
})
|
||||
|
||||
const createMockChildChunk = (id: string, content: string, edited = false): ChildChunkDetail => ({
|
||||
id,
|
||||
content,
|
||||
position: 1,
|
||||
word_count: 10,
|
||||
segment_id: 'seg-1',
|
||||
created_at: Date.now(),
|
||||
updated_at: edited ? Date.now() + 1000 : Date.now(),
|
||||
type: 'automatic',
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
childChunks: [createMockChildChunk('child-1', 'Child content 1')],
|
||||
parentChunkId: 'parent-1',
|
||||
handleInputChange: vi.fn(),
|
||||
handleAddNewChildChunk: vi.fn(),
|
||||
enabled: true,
|
||||
onDelete: vi.fn(),
|
||||
onClickSlice: vi.fn(),
|
||||
total: 1,
|
||||
inputValue: '',
|
||||
onClearFilter: vi.fn(),
|
||||
isLoading: false,
|
||||
focused: false,
|
||||
}
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render with empty child chunks', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render total count text', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText(/child chunks/i)).toBeInTheDocument()
|
||||
// Assert
|
||||
expect(screen.getByText(/segment\.childChunks/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render child chunks when provided', () => {
|
||||
const childChunks = [
|
||||
createMockChildChunk({ id: 'child-1', position: 1, content: 'First chunk' }),
|
||||
createMockChildChunk({ id: 'child-2', position: 2, content: 'Second chunk' }),
|
||||
]
|
||||
it('should render add button', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={childChunks} />)
|
||||
|
||||
// In paragraph mode, content is collapsed by default
|
||||
expect(screen.getByText(/2 child chunks/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render total count correctly with total prop in full-doc mode', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const childChunks = [createMockChildChunk()]
|
||||
|
||||
// Pass inputValue="" to ensure isSearching is false
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={childChunks} total={5} isLoading={false} inputValue="" />)
|
||||
|
||||
expect(screen.getByText(/5 child chunks/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading skeleton in full-doc mode when loading', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} isLoading={true} />)
|
||||
|
||||
expect(screen.getByTestId('full-doc-list-skeleton')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render loading skeleton when not loading', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} isLoading={false} />)
|
||||
|
||||
expect(screen.queryByTestId('full-doc-list-skeleton')).not.toBeInTheDocument()
|
||||
// Assert
|
||||
expect(screen.getByText(/operation\.add/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Paragraph mode tests
|
||||
describe('Paragraph Mode', () => {
|
||||
beforeEach(() => {
|
||||
mockParentMode.current = 'paragraph'
|
||||
mockParentMode = 'paragraph'
|
||||
})
|
||||
|
||||
it('should show collapse icon in paragraph mode', () => {
|
||||
const childChunks = [createMockChildChunk()]
|
||||
it('should render collapsed by default in paragraph mode', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={childChunks} />)
|
||||
|
||||
// Check for collapse/expand behavior
|
||||
const totalRow = screen.getByText(/1 child chunk/i).closest('div')
|
||||
expect(totalRow).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle collapsed state when clicked', () => {
|
||||
const childChunks = [createMockChildChunk({ content: 'Test content' })]
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={childChunks} />)
|
||||
|
||||
// Initially collapsed in paragraph mode - content should not be visible
|
||||
// Assert - collapsed icon should be present
|
||||
expect(screen.queryByTestId('formatted-text')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Find and click the toggle area
|
||||
const toggleArea = screen.getByText(/1 child chunk/i).closest('div')
|
||||
it('should expand when clicking toggle in paragraph mode', () => {
|
||||
// Arrange
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
// Click to expand
|
||||
// Act - click on the collapse toggle
|
||||
const toggleArea = screen.getByText(/segment\.childChunks/i).closest('div')
|
||||
if (toggleArea)
|
||||
fireEvent.click(toggleArea)
|
||||
|
||||
// After expansion, content should be visible
|
||||
// Assert - child chunks should be visible
|
||||
expect(screen.getByTestId('formatted-text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply opacity when disabled', () => {
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} enabled={false} />)
|
||||
it('should collapse when clicking toggle again', () => {
|
||||
// Arrange
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
const wrapper = container.firstChild
|
||||
expect(wrapper).toHaveClass('opacity-50')
|
||||
})
|
||||
// Act - click twice
|
||||
const toggleArea = screen.getByText(/segment\.childChunks/i).closest('div')
|
||||
if (toggleArea) {
|
||||
fireEvent.click(toggleArea)
|
||||
fireEvent.click(toggleArea)
|
||||
}
|
||||
|
||||
it('should not apply opacity when enabled', () => {
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} enabled={true} />)
|
||||
|
||||
const wrapper = container.firstChild
|
||||
expect(wrapper).not.toHaveClass('opacity-50')
|
||||
// Assert - child chunks should be hidden
|
||||
expect(screen.queryByTestId('formatted-text')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Full-Doc Mode', () => {
|
||||
// Full doc mode tests
|
||||
describe('Full Doc Mode', () => {
|
||||
beforeEach(() => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
mockParentMode = 'full-doc'
|
||||
})
|
||||
|
||||
it('should show content by default in full-doc mode', () => {
|
||||
const childChunks = [createMockChildChunk({ content: 'Full doc content' })]
|
||||
it('should render input field in full-doc mode', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={childChunks} isLoading={false} />)
|
||||
|
||||
expect(screen.getByTestId('formatted-text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render search input in full-doc mode', () => {
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="" handleInputChange={vi.fn()} />)
|
||||
|
||||
const input = document.querySelector('input')
|
||||
// Assert
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render child chunks without collapse in full-doc mode', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('formatted-text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call handleInputChange when input changes', () => {
|
||||
const handleInputChange = vi.fn()
|
||||
// Arrange
|
||||
const mockHandleInputChange = vi.fn()
|
||||
render(<ChildSegmentList {...defaultProps} handleInputChange={mockHandleInputChange} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="" handleInputChange={handleInputChange} />)
|
||||
// Act
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'search term' } })
|
||||
|
||||
const input = document.querySelector('input')
|
||||
if (input) {
|
||||
fireEvent.change(input, { target: { value: 'test search' } })
|
||||
expect(handleInputChange).toHaveBeenCalledWith('test search')
|
||||
}
|
||||
// Assert
|
||||
expect(mockHandleInputChange).toHaveBeenCalledWith('search term')
|
||||
})
|
||||
|
||||
it('should show search results text when searching', () => {
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="search term" total={3} />)
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="search" total={5} />)
|
||||
|
||||
expect(screen.getByText(/3 search results/i)).toBeInTheDocument()
|
||||
// Assert
|
||||
expect(screen.getByText(/segment\.searchResults/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show empty component when no results and searching', () => {
|
||||
render(
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
childChunks={[]}
|
||||
inputValue="search term"
|
||||
onClearFilter={vi.fn()}
|
||||
isLoading={false}
|
||||
/>,
|
||||
)
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="search" childChunks={[]} total={0} />)
|
||||
|
||||
expect(screen.getByTestId('empty-component')).toBeInTheDocument()
|
||||
// Assert
|
||||
expect(screen.getByTestId('empty')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClearFilter when clear button clicked in empty state', () => {
|
||||
const onClearFilter = vi.fn()
|
||||
it('should show loading skeleton when isLoading is true', () => {
|
||||
// Arrange & Act
|
||||
render(<ChildSegmentList {...defaultProps} isLoading={true} />)
|
||||
|
||||
render(
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
childChunks={[]}
|
||||
inputValue="search term"
|
||||
onClearFilter={onClearFilter}
|
||||
isLoading={false}
|
||||
/>,
|
||||
)
|
||||
// Assert
|
||||
expect(screen.getByTestId('full-doc-skeleton')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const clearButton = screen.getByText('Clear Filter')
|
||||
fireEvent.click(clearButton)
|
||||
it('should handle undefined total in full-doc mode', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} total={undefined} />)
|
||||
|
||||
expect(onClearFilter).toHaveBeenCalled()
|
||||
// Assert - component should render without crashing
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Child Chunk Items', () => {
|
||||
it('should render edited label when chunk is edited', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const editedChunk = createMockChildChunk({
|
||||
id: 'edited-chunk',
|
||||
position: 1,
|
||||
created_at: 1700000000,
|
||||
updated_at: 1700000001, // Different from created_at
|
||||
})
|
||||
// User Interactions
|
||||
describe('User Interactions', () => {
|
||||
it('should call handleAddNewChildChunk when add button is clicked', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const mockHandleAddNewChildChunk = vi.fn()
|
||||
render(<ChildSegmentList {...defaultProps} handleAddNewChildChunk={mockHandleAddNewChildChunk} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={[editedChunk]} isLoading={false} />)
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.add/i))
|
||||
|
||||
expect(screen.getByText(/C-1 · edited/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show edited label when chunk is not edited', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const normalChunk = createMockChildChunk({
|
||||
id: 'normal-chunk',
|
||||
position: 2,
|
||||
created_at: 1700000000,
|
||||
updated_at: 1700000000, // Same as created_at
|
||||
})
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={[normalChunk]} isLoading={false} />)
|
||||
|
||||
expect(screen.getByText('C-2')).toBeInTheDocument()
|
||||
expect(screen.queryByText(/edited/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClickSlice when chunk is clicked', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const onClickSlice = vi.fn()
|
||||
const chunk = createMockChildChunk({ id: 'clickable-chunk' })
|
||||
|
||||
render(
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
childChunks={[chunk]}
|
||||
onClickSlice={onClickSlice}
|
||||
isLoading={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const editSlice = screen.getByTestId('edit-slice')
|
||||
fireEvent.click(editSlice)
|
||||
|
||||
expect(onClickSlice).toHaveBeenCalledWith(chunk)
|
||||
// Assert
|
||||
expect(mockHandleAddNewChildChunk).toHaveBeenCalledWith('parent-1')
|
||||
})
|
||||
|
||||
it('should call onDelete when delete button is clicked', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const onDelete = vi.fn()
|
||||
const chunk = createMockChildChunk({ id: 'deletable-chunk', segment_id: 'seg-1' })
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const mockOnDelete = vi.fn()
|
||||
render(<ChildSegmentList {...defaultProps} onDelete={mockOnDelete} />)
|
||||
|
||||
render(
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
childChunks={[chunk]}
|
||||
onDelete={onDelete}
|
||||
isLoading={false}
|
||||
/>,
|
||||
)
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('delete-slice-btn'))
|
||||
|
||||
const deleteButton = screen.getByTestId('delete-button')
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
expect(onDelete).toHaveBeenCalledWith('seg-1', 'deletable-chunk')
|
||||
// Assert
|
||||
expect(mockOnDelete).toHaveBeenCalledWith('seg-1', 'child-1')
|
||||
})
|
||||
|
||||
it('should apply focused styles when chunk is currently selected', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const chunk = createMockChildChunk({ id: 'focused-chunk' })
|
||||
mockCurrChildChunk.current = { childChunkInfo: chunk, showModal: true }
|
||||
it('should call onClickSlice when slice is clicked', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const mockOnClickSlice = vi.fn()
|
||||
render(<ChildSegmentList {...defaultProps} onClickSlice={mockOnClickSlice} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={[chunk]} isLoading={false} />)
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('click-slice-btn'))
|
||||
|
||||
const label = screen.getByTestId('edit-slice-label')
|
||||
// Assert
|
||||
expect(mockOnClickSlice).toHaveBeenCalledWith(expect.objectContaining({ id: 'child-1' }))
|
||||
})
|
||||
|
||||
it('should call onClearFilter when clear filter button is clicked', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const mockOnClearFilter = vi.fn()
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="search" childChunks={[]} onClearFilter={mockOnClearFilter} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('clear-filter-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnClearFilter).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Focused state
|
||||
describe('Focused State', () => {
|
||||
it('should apply focused style when currChildChunk matches', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
mockCurrChildChunk = { childChunkInfo: { id: 'child-1' } }
|
||||
|
||||
// Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
// Assert - check for focused class on label
|
||||
const label = screen.getByTestId('slice-label')
|
||||
expect(label).toHaveClass('bg-state-accent-solid')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add Button', () => {
|
||||
it('should call handleAddNewChildChunk when Add button is clicked', () => {
|
||||
const handleAddNewChildChunk = vi.fn()
|
||||
it('should not apply focused style when currChildChunk does not match', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
mockCurrChildChunk = { childChunkInfo: { id: 'other-child' } }
|
||||
|
||||
render(
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
parentChunkId="parent-123"
|
||||
/>,
|
||||
)
|
||||
// Act
|
||||
render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
const addButton = screen.getByText('Add')
|
||||
fireEvent.click(addButton)
|
||||
|
||||
expect(handleAddNewChildChunk).toHaveBeenCalledWith('parent-123')
|
||||
})
|
||||
|
||||
it('should disable Add button when loading in full-doc mode', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} isLoading={true} />)
|
||||
|
||||
const addButton = screen.getByText('Add')
|
||||
expect(addButton).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should stop propagation when Add button is clicked', () => {
|
||||
const handleAddNewChildChunk = vi.fn()
|
||||
const parentClickHandler = vi.fn()
|
||||
|
||||
render(
|
||||
<div onClick={parentClickHandler}>
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
handleAddNewChildChunk={handleAddNewChildChunk}
|
||||
/>
|
||||
</div>,
|
||||
)
|
||||
|
||||
const addButton = screen.getByText('Add')
|
||||
fireEvent.click(addButton)
|
||||
|
||||
expect(handleAddNewChildChunk).toHaveBeenCalled()
|
||||
// Parent should not be called due to stopPropagation
|
||||
// Assert
|
||||
const label = screen.getByTestId('slice-label')
|
||||
expect(label).not.toHaveClass('bg-state-accent-solid')
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeTotalInfo function', () => {
|
||||
it('should return search results when searching in full-doc mode', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
// Enabled/Disabled state
|
||||
describe('Enabled State', () => {
|
||||
it('should apply opacity when enabled is false', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} enabled={false} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} inputValue="search" total={10} />)
|
||||
|
||||
expect(screen.getByText(/10 search results/i)).toBeInTheDocument()
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('opacity-50')
|
||||
})
|
||||
|
||||
it('should return "--" when total is 0 in full-doc mode', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
it('should not apply opacity when enabled is true', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} enabled={true} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} total={0} />)
|
||||
|
||||
// When total is 0, displayText is '--'
|
||||
expect(screen.getByText(/--/)).toBeInTheDocument()
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).not.toHaveClass('opacity-50')
|
||||
})
|
||||
|
||||
it('should use childChunks length in paragraph mode', () => {
|
||||
mockParentMode.current = 'paragraph'
|
||||
const childChunks = [
|
||||
createMockChildChunk(),
|
||||
createMockChildChunk(),
|
||||
createMockChildChunk(),
|
||||
]
|
||||
it('should not apply opacity when focused is true even if enabled is false', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} enabled={false} focused={true} />)
|
||||
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={childChunks} />)
|
||||
|
||||
expect(screen.getByText(/3 child chunks/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Focused State', () => {
|
||||
it('should not apply opacity when focused even if disabled', () => {
|
||||
const { container } = render(
|
||||
<ChildSegmentList {...defaultProps} enabled={false} focused={true} />,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).not.toHaveClass('opacity-50')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Input clear button', () => {
|
||||
it('should call handleInputChange with empty string when clear is clicked', () => {
|
||||
mockParentMode.current = 'full-doc'
|
||||
const handleInputChange = vi.fn()
|
||||
// Edited indicator
|
||||
describe('Edited Indicator', () => {
|
||||
it('should show edited indicator for edited chunks', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const editedChunk = createMockChildChunk('child-edited', 'Edited content', true)
|
||||
|
||||
render(
|
||||
<ChildSegmentList
|
||||
{...defaultProps}
|
||||
inputValue="test"
|
||||
handleInputChange={handleInputChange}
|
||||
/>,
|
||||
)
|
||||
// Act
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={[editedChunk]} />)
|
||||
|
||||
// Find the clear button (it's the showClearIcon button in Input)
|
||||
const input = document.querySelector('input')
|
||||
if (input) {
|
||||
// Trigger clear by simulating the input's onClear
|
||||
const clearButton = document.querySelector('[class*="cursor-pointer"]')
|
||||
if (clearButton)
|
||||
fireEvent.click(clearButton)
|
||||
}
|
||||
// Assert
|
||||
const label = screen.getByTestId('slice-label')
|
||||
expect(label.textContent).toContain('segment.edited')
|
||||
})
|
||||
})
|
||||
|
||||
// Multiple chunks
|
||||
describe('Multiple Chunks', () => {
|
||||
it('should render multiple child chunks', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const chunks = [
|
||||
createMockChildChunk('child-1', 'Content 1'),
|
||||
createMockChildChunk('child-2', 'Content 2'),
|
||||
createMockChildChunk('child-3', 'Content 3'),
|
||||
]
|
||||
|
||||
// Act
|
||||
render(<ChildSegmentList {...defaultProps} childChunks={chunks} total={3} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getAllByTestId('edit-slice')).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty childChunks array', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
|
||||
// Act
|
||||
const { container } = render(<ChildSegmentList {...defaultProps} childChunks={[]} />)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain structure when rerendered', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
const { rerender } = render(<ChildSegmentList {...defaultProps} />)
|
||||
|
||||
// Act
|
||||
const newChunks = [createMockChildChunk('new-child', 'New content')]
|
||||
rerender(<ChildSegmentList {...defaultProps} childChunks={newChunks} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('New content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable add button when loading', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
|
||||
// Act
|
||||
render(<ChildSegmentList {...defaultProps} isLoading={true} />)
|
||||
|
||||
// Assert
|
||||
const addButton = screen.getByText(/operation\.add/i)
|
||||
expect(addButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,523 @@
|
||||
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'
|
||||
|
||||
// Mock useKeyPress from ahooks to capture and test callback functions
|
||||
const mockUseKeyPress = vi.fn()
|
||||
vi.mock('ahooks', () => ({
|
||||
useKeyPress: (keys: string | string[], callback: (e: KeyboardEvent) => void, options?: object) => {
|
||||
mockUseKeyPress(keys, callback, options)
|
||||
},
|
||||
}))
|
||||
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to get captured callbacks from useKeyPress mock
|
||||
const getEscCallback = (): ((e: KeyboardEvent) => void) | undefined => {
|
||||
const escCall = mockUseKeyPress.mock.calls.find(
|
||||
(call) => {
|
||||
const keys = call[0]
|
||||
return Array.isArray(keys) && keys.includes('esc')
|
||||
},
|
||||
)
|
||||
return escCall?.[1]
|
||||
}
|
||||
|
||||
const getCtrlSCallback = (): ((e: KeyboardEvent) => void) | undefined => {
|
||||
const ctrlSCall = mockUseKeyPress.mock.calls.find(
|
||||
(call) => {
|
||||
const keys = call[0]
|
||||
return typeof keys === 'string' && keys.includes('.s')
|
||||
},
|
||||
)
|
||||
return ctrlSCall?.[1]
|
||||
}
|
||||
|
||||
describe('ActionButtons', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseKeyPress.mockClear()
|
||||
})
|
||||
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
||||
// Keyboard shortcuts tests via useKeyPress callbacks
|
||||
describe('Keyboard Shortcuts', () => {
|
||||
it('should display ctrl key hint on save button', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ActionButtons
|
||||
handleCancel={vi.fn()}
|
||||
handleSave={vi.fn()}
|
||||
loading={false}
|
||||
/>,
|
||||
{ wrapper: createWrapper({}) },
|
||||
)
|
||||
|
||||
// Assert - check for ctrl key hint (Ctrl or Cmd depending on system)
|
||||
const kbdElements = document.querySelectorAll('.system-kbd')
|
||||
expect(kbdElements.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should call handleCancel and preventDefault when ESC key is pressed', () => {
|
||||
// Arrange
|
||||
const mockHandleCancel = vi.fn()
|
||||
const mockPreventDefault = vi.fn()
|
||||
render(
|
||||
<ActionButtons
|
||||
handleCancel={mockHandleCancel}
|
||||
handleSave={vi.fn()}
|
||||
loading={false}
|
||||
/>,
|
||||
{ wrapper: createWrapper({}) },
|
||||
)
|
||||
|
||||
// Act - get the ESC callback and invoke it
|
||||
const escCallback = getEscCallback()
|
||||
expect(escCallback).toBeDefined()
|
||||
escCallback!({ preventDefault: mockPreventDefault } as unknown as KeyboardEvent)
|
||||
|
||||
// Assert
|
||||
expect(mockPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call handleSave and preventDefault when Ctrl+S is pressed and not loading', () => {
|
||||
// Arrange
|
||||
const mockHandleSave = vi.fn()
|
||||
const mockPreventDefault = vi.fn()
|
||||
render(
|
||||
<ActionButtons
|
||||
handleCancel={vi.fn()}
|
||||
handleSave={mockHandleSave}
|
||||
loading={false}
|
||||
/>,
|
||||
{ wrapper: createWrapper({}) },
|
||||
)
|
||||
|
||||
// Act - get the Ctrl+S callback and invoke it
|
||||
const ctrlSCallback = getCtrlSCallback()
|
||||
expect(ctrlSCallback).toBeDefined()
|
||||
ctrlSCallback!({ preventDefault: mockPreventDefault } as unknown as KeyboardEvent)
|
||||
|
||||
// Assert
|
||||
expect(mockPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleSave).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call handleSave when Ctrl+S is pressed while loading', () => {
|
||||
// Arrange
|
||||
const mockHandleSave = vi.fn()
|
||||
const mockPreventDefault = vi.fn()
|
||||
render(
|
||||
<ActionButtons
|
||||
handleCancel={vi.fn()}
|
||||
handleSave={mockHandleSave}
|
||||
loading={true}
|
||||
/>,
|
||||
{ wrapper: createWrapper({}) },
|
||||
)
|
||||
|
||||
// Act - get the Ctrl+S callback and invoke it
|
||||
const ctrlSCallback = getCtrlSCallback()
|
||||
expect(ctrlSCallback).toBeDefined()
|
||||
ctrlSCallback!({ preventDefault: mockPreventDefault } as unknown as KeyboardEvent)
|
||||
|
||||
// Assert
|
||||
expect(mockPreventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleSave).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should register useKeyPress with correct options for Ctrl+S', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ActionButtons
|
||||
handleCancel={vi.fn()}
|
||||
handleSave={vi.fn()}
|
||||
loading={false}
|
||||
/>,
|
||||
{ wrapper: createWrapper({}) },
|
||||
)
|
||||
|
||||
// Assert - verify useKeyPress was called with correct options
|
||||
const ctrlSCall = mockUseKeyPress.mock.calls.find(
|
||||
call => typeof call[0] === 'string' && call[0].includes('.s'),
|
||||
)
|
||||
expect(ctrlSCall).toBeDefined()
|
||||
expect(ctrlSCall![2]).toEqual({ exactMatch: true, useCapture: true })
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,317 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import ChunkContent from './chunk-content'
|
||||
|
||||
// Mock ResizeObserver
|
||||
const OriginalResizeObserver = globalThis.ResizeObserver
|
||||
class MockResizeObserver {
|
||||
observe = vi.fn()
|
||||
disconnect = vi.fn()
|
||||
unobserve = vi.fn()
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
globalThis.ResizeObserver = MockResizeObserver as typeof ResizeObserver
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
globalThis.ResizeObserver = OriginalResizeObserver
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,129 +1,153 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Empty from './empty'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
if (key === 'segment.empty')
|
||||
return 'No results found'
|
||||
if (key === 'segment.clearFilter')
|
||||
return 'Clear Filter'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Empty Component', () => {
|
||||
const defaultProps = {
|
||||
onClearFilter: vi.fn(),
|
||||
}
|
||||
|
||||
describe('Empty', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render empty state message', () => {
|
||||
render(<Empty {...defaultProps} />)
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('No results found')).toBeInTheDocument()
|
||||
// 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', () => {
|
||||
render(<Empty {...defaultProps} />)
|
||||
// Arrange & Act
|
||||
render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('Clear Filter')).toBeInTheDocument()
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon', () => {
|
||||
const { container } = render(<Empty {...defaultProps} />)
|
||||
it('should render background empty cards', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
// Check for the icon container
|
||||
// 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 decorative lines', () => {
|
||||
const { container } = render(<Empty {...defaultProps} />)
|
||||
it('should render clear filter button with accent text styling', () => {
|
||||
// Arrange & Act
|
||||
render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
// Check for SVG lines
|
||||
const svgs = container.querySelectorAll('svg')
|
||||
expect(svgs.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render background cards', () => {
|
||||
const { container } = render(<Empty {...defaultProps} />)
|
||||
|
||||
// Check for background empty cards (10 of them)
|
||||
const backgroundCards = container.querySelectorAll('.rounded-xl.bg-background-section-burn')
|
||||
expect(backgroundCards.length).toBe(10)
|
||||
})
|
||||
|
||||
it('should render mask overlay', () => {
|
||||
const { container } = render(<Empty {...defaultProps} />)
|
||||
|
||||
const maskOverlay = container.querySelector('.bg-dataset-chunk-list-mask-bg')
|
||||
expect(maskOverlay).toBeInTheDocument()
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toHaveClass('text-text-accent')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Interactions', () => {
|
||||
it('should call onClearFilter when clear filter button is clicked', () => {
|
||||
const onClearFilter = vi.fn()
|
||||
// Props tests
|
||||
describe('Props', () => {
|
||||
it('should accept onClearFilter callback prop', () => {
|
||||
// Arrange
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
render(<Empty onClearFilter={onClearFilter} />)
|
||||
// Act
|
||||
render(<Empty onClearFilter={mockCallback} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
const clearButton = screen.getByText('Clear Filter')
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
expect(onClearFilter).toHaveBeenCalledTimes(1)
|
||||
// Assert
|
||||
expect(mockCallback).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized', () => {
|
||||
// Empty is wrapped with React.memo
|
||||
const { rerender } = render(<Empty {...defaultProps} />)
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle multiple clicks on clear filter button', () => {
|
||||
// Arrange
|
||||
const mockOnClearFilter = vi.fn()
|
||||
render(<Empty onClearFilter={mockOnClearFilter} />)
|
||||
|
||||
// Same props should not cause re-render issues
|
||||
rerender(<Empty {...defaultProps} />)
|
||||
// Act
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
|
||||
expect(screen.getByText('No results found')).toBeInTheDocument()
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('EmptyCard Component', () => {
|
||||
it('should render within Empty component', () => {
|
||||
const { container } = render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
// EmptyCard renders as background cards
|
||||
const emptyCards = container.querySelectorAll('.h-32.w-full')
|
||||
expect(emptyCards.length).toBe(10)
|
||||
})
|
||||
|
||||
it('should have correct opacity', () => {
|
||||
const { container } = render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
const emptyCards = container.querySelectorAll('.opacity-30')
|
||||
expect(emptyCards.length).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Line Component', () => {
|
||||
it('should render SVG lines within Empty component', () => {
|
||||
const { container } = render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
// Line components render as SVG elements (4 Line components + 1 icon SVG)
|
||||
const lines = container.querySelectorAll('svg')
|
||||
expect(lines.length).toBeGreaterThanOrEqual(4)
|
||||
})
|
||||
|
||||
it('should have gradient definition', () => {
|
||||
const { container } = render(<Empty onClearFilter={vi.fn()} />)
|
||||
|
||||
const gradients = container.querySelectorAll('linearGradient')
|
||||
expect(gradients.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,262 @@
|
||||
import type { ReactNode } from 'react'
|
||||
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: 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,317 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
// TagInput callback tests
|
||||
describe('TagInput Callback', () => {
|
||||
it('should call onKeywordsChange when keywords are modified', () => {
|
||||
// Arrange
|
||||
const mockOnKeywordsChange = vi.fn()
|
||||
render(
|
||||
<Keywords
|
||||
segInfo={{ id: '1', keywords: ['existing'] }}
|
||||
keywords={['existing']}
|
||||
onKeywordsChange={mockOnKeywordsChange}
|
||||
isEditMode={true}
|
||||
actionType="edit"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - TagInput should be rendered
|
||||
expect(screen.queryByText('-')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable add when isEditMode is false', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Keywords
|
||||
segInfo={{ id: '1', keywords: ['test'] }}
|
||||
keywords={['test']}
|
||||
onKeywordsChange={vi.fn()}
|
||||
isEditMode={false}
|
||||
actionType="view"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - TagInput should exist but with disabled add
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable remove when only one keyword exists in edit mode', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Keywords
|
||||
segInfo={{ id: '1', keywords: ['only-one'] }}
|
||||
keywords={['only-one']}
|
||||
onKeywordsChange={vi.fn()}
|
||||
isEditMode={true}
|
||||
actionType="edit"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - component should render
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should allow remove when multiple keywords exist in edit mode', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(
|
||||
<Keywords
|
||||
segInfo={{ id: '1', keywords: ['first', 'second'] }}
|
||||
keywords={['first', 'second']}
|
||||
onKeywordsChange={vi.fn()}
|
||||
isEditMode={true}
|
||||
actionType="edit"
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - component should render
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,327 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { EventEmitterContextProvider, useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import RegenerationModal from './regeneration-modal'
|
||||
|
||||
// Store emit function for triggering events in tests
|
||||
let emitFunction: ((v: string) => void) | null = null
|
||||
|
||||
const EmitCapture = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
emitFunction = eventEmitter?.emit?.bind(eventEmitter) || null
|
||||
return null
|
||||
}
|
||||
|
||||
// Custom wrapper that captures emit function
|
||||
const TestWrapper = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<EventEmitterContextProvider>
|
||||
<EmitCapture />
|
||||
{children}
|
||||
</EventEmitterContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// Create a wrapper component with event emitter context
|
||||
const createWrapper = () => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<TestWrapper>
|
||||
{children}
|
||||
</TestWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
<TestWrapper>
|
||||
<RegenerationModal {...defaultProps} isShow={false} />
|
||||
</TestWrapper>,
|
||||
)
|
||||
|
||||
// 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(
|
||||
<TestWrapper>
|
||||
<RegenerationModal {...defaultProps} onConfirm={mockOnConfirm} />
|
||||
</TestWrapper>,
|
||||
)
|
||||
fireEvent.click(screen.getByText(/operation\.regenerate/i))
|
||||
|
||||
// Assert
|
||||
expect(mockOnConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Loading state
|
||||
describe('Loading State', () => {
|
||||
it('should show regenerating content when update-segment event is emitted', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
if (emitFunction)
|
||||
emitFunction('update-segment')
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/segment\.regeneratingTitle/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show regenerating message during loading', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
if (emitFunction)
|
||||
emitFunction('update-segment')
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/segment\.regeneratingMessage/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable regenerate button during loading', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
if (emitFunction)
|
||||
emitFunction('update-segment')
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const button = screen.getByText(/operation\.regenerate/i).closest('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Success state
|
||||
describe('Success State', () => {
|
||||
it('should show success content when update-segment-success event is emitted followed by done', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act - trigger loading then success then done
|
||||
act(() => {
|
||||
if (emitFunction) {
|
||||
emitFunction('update-segment')
|
||||
emitFunction('update-segment-success')
|
||||
emitFunction('update-segment-done')
|
||||
}
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/segment\.regenerationSuccessTitle/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show success message when completed', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
if (emitFunction) {
|
||||
emitFunction('update-segment')
|
||||
emitFunction('update-segment-success')
|
||||
emitFunction('update-segment-done')
|
||||
}
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/segment\.regenerationSuccessMessage/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show close button with countdown in success state', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
if (emitFunction) {
|
||||
emitFunction('update-segment')
|
||||
emitFunction('update-segment-success')
|
||||
emitFunction('update-segment-done')
|
||||
}
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/operation\.close/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onClose when close button is clicked in success state', async () => {
|
||||
// Arrange
|
||||
const mockOnClose = vi.fn()
|
||||
render(<RegenerationModal {...defaultProps} onClose={mockOnClose} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
if (emitFunction) {
|
||||
emitFunction('update-segment')
|
||||
emitFunction('update-segment-success')
|
||||
emitFunction('update-segment-done')
|
||||
}
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/operation\.close/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/operation\.close/i))
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// State transitions
|
||||
describe('State Transitions', () => {
|
||||
it('should return to default content when update fails (no success event)', async () => {
|
||||
// Arrange
|
||||
render(<RegenerationModal {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
// Act - trigger loading then done without success
|
||||
act(() => {
|
||||
if (emitFunction) {
|
||||
emitFunction('update-segment')
|
||||
emitFunction('update-segment-done')
|
||||
}
|
||||
})
|
||||
|
||||
// Assert - should show default content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/segment\.regenerationConfirmTitle/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,507 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
// Add another behavior
|
||||
describe('Add Another Behavior', () => {
|
||||
it('should close modal when add another is unchecked after save', async () => {
|
||||
// Arrange
|
||||
const mockOnCancel = vi.fn()
|
||||
mockAddChildSegment.mockImplementation((_params, options) => {
|
||||
options.onSuccess({ data: { id: 'new-child-id' } })
|
||||
options.onSettled()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<NewChildSegmentModal {...defaultProps} onCancel={mockOnCancel} />)
|
||||
|
||||
// Uncheck add another
|
||||
fireEvent.click(screen.getByTestId('add-another-checkbox'))
|
||||
|
||||
// Enter valid content
|
||||
fireEvent.change(screen.getByTestId('content-input'), {
|
||||
target: { value: 'Valid content' },
|
||||
})
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
|
||||
// Assert - modal should close
|
||||
await waitFor(() => {
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not close modal when add another is checked after save', async () => {
|
||||
// Arrange
|
||||
const mockOnCancel = vi.fn()
|
||||
mockAddChildSegment.mockImplementation((_params, options) => {
|
||||
options.onSuccess({ data: { id: 'new-child-id' } })
|
||||
options.onSettled()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<NewChildSegmentModal {...defaultProps} onCancel={mockOnCancel} />)
|
||||
|
||||
// Enter valid content (add another is checked by default)
|
||||
fireEvent.change(screen.getByTestId('content-input'), {
|
||||
target: { value: 'Valid content' },
|
||||
})
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
|
||||
// Assert - modal should not close, only content cleared
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('content-input')).toHaveValue('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// View newly added chunk
|
||||
describe('View Newly Added Chunk', () => {
|
||||
it('should show custom button in full-doc mode after save', async () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
mockAddChildSegment.mockImplementation((_params, options) => {
|
||||
options.onSuccess({ data: { id: 'new-child-id' } })
|
||||
options.onSettled()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<NewChildSegmentModal {...defaultProps} />)
|
||||
|
||||
// Enter valid content
|
||||
fireEvent.change(screen.getByTestId('content-input'), {
|
||||
target: { value: 'Valid content' },
|
||||
})
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
|
||||
// Assert - success notification with custom component
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'success',
|
||||
customComponent: expect.anything(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not show custom button in paragraph mode after save', async () => {
|
||||
// Arrange
|
||||
mockParentMode = 'paragraph'
|
||||
const mockOnSave = vi.fn()
|
||||
mockAddChildSegment.mockImplementation((_params, options) => {
|
||||
options.onSuccess({ data: { id: 'new-child-id' } })
|
||||
options.onSettled()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<NewChildSegmentModal {...defaultProps} onSave={mockOnSave} />)
|
||||
|
||||
// Enter valid content
|
||||
fireEvent.change(screen.getByTestId('content-input'), {
|
||||
target: { value: 'Valid content' },
|
||||
})
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
|
||||
// Assert - onSave should be called with data
|
||||
await waitFor(() => {
|
||||
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({ id: 'new-child-id' }))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Cancel behavior
|
||||
describe('Cancel Behavior', () => {
|
||||
it('should call onCancel when close button is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnCancel = vi.fn()
|
||||
render(<NewChildSegmentModal {...defaultProps} onCancel={mockOnCancel} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,270 @@
|
||||
import type { ReactNode } from 'react'
|
||||
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: 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,679 @@
|
||||
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, onChange }: { value?: unknown[], onChange?: (v: unknown[]) => void, disabled?: boolean }) => {
|
||||
return (
|
||||
<div data-testid="image-uploader">
|
||||
<span data-testid="uploader-disabled">{disabled ? 'disabled' : 'enabled'}</span>
|
||||
<span data-testid="attachments-count">{value?.length || 0}</span>
|
||||
<button
|
||||
data-testid="add-attachment-btn"
|
||||
onClick={() => onChange?.([...(value || []), { id: 'new-attachment' }])}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</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()
|
||||
})
|
||||
})
|
||||
|
||||
// Attachments
|
||||
describe('Attachments', () => {
|
||||
it('should update attachments when onChange is called', () => {
|
||||
// Arrange
|
||||
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('add-attachment-btn'))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('attachments-count')).toHaveTextContent('1')
|
||||
})
|
||||
|
||||
it('should pass attachments to onUpdate when save is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = vi.fn()
|
||||
render(<SegmentDetail {...defaultProps} isEditMode={true} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Add an attachment
|
||||
fireEvent.click(screen.getByTestId('add-attachment-btn'))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('save-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(
|
||||
'segment-1',
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(Array),
|
||||
expect.arrayContaining([expect.objectContaining({ id: 'new-attachment' })]),
|
||||
)
|
||||
})
|
||||
|
||||
it('should initialize attachments from segInfo', () => {
|
||||
// Arrange
|
||||
const segInfoWithAttachments = {
|
||||
...defaultSegInfo,
|
||||
attachments: [
|
||||
{ id: 'att-1', name: 'file1.jpg', size: 1000, mime_type: 'image/jpeg', extension: 'jpg', source_url: 'http://example.com/file1.jpg' },
|
||||
],
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<SegmentDetail {...defaultProps} segInfo={segInfoWithAttachments} isEditMode={true} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('attachments-count')).toHaveTextContent('1')
|
||||
})
|
||||
})
|
||||
|
||||
// Regeneration confirmation
|
||||
describe('Regeneration Confirmation', () => {
|
||||
it('should call onUpdate with needRegenerate true when confirm regeneration is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = vi.fn()
|
||||
render(<SegmentDetail {...defaultProps} isEditMode={true} onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Open regeneration modal
|
||||
fireEvent.click(screen.getByTestId('regenerate-btn'))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('confirm-regeneration'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(
|
||||
'segment-1',
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(Array),
|
||||
expect.any(Array),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('should close modal and edit drawer when close after regeneration is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnModalStateChange = vi.fn()
|
||||
render(
|
||||
<SegmentDetail
|
||||
{...defaultProps}
|
||||
isEditMode={true}
|
||||
onCancel={mockOnCancel}
|
||||
onModalStateChange={mockOnModalStateChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Open regeneration modal
|
||||
fireEvent.click(screen.getByTestId('regenerate-btn'))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('close-regeneration'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnModalStateChange).toHaveBeenCalledWith(false)
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// QA mode
|
||||
describe('QA Mode', () => {
|
||||
it('should render answer input in QA mode', () => {
|
||||
// Arrange & Act
|
||||
render(<SegmentDetail {...defaultProps} docForm={ChunkingMode.qa} isEditMode={true} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('answer-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update answer when input changes', () => {
|
||||
// Arrange
|
||||
render(<SegmentDetail {...defaultProps} docForm={ChunkingMode.qa} isEditMode={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByTestId('answer-input'), {
|
||||
target: { value: 'Updated answer' },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('answer-input')).toHaveValue('Updated answer')
|
||||
})
|
||||
|
||||
it('should calculate word count correctly in QA mode', () => {
|
||||
// Arrange & Act
|
||||
render(<SegmentDetail {...defaultProps} docForm={ChunkingMode.qa} isEditMode={true} />)
|
||||
|
||||
// Assert - should show combined length of question and answer
|
||||
expect(screen.getByText(/segment\.characters/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Full doc mode
|
||||
describe('Full Doc Mode', () => {
|
||||
it('should show label in full-doc parent-child mode', () => {
|
||||
// Arrange
|
||||
mockParentMode = 'full-doc'
|
||||
|
||||
// Act
|
||||
render(<SegmentDetail {...defaultProps} docForm={ChunkingMode.parentChild} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('segment-index-tag')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Keywords update
|
||||
describe('Keywords Update', () => {
|
||||
it('should update keywords when changed in edit mode', () => {
|
||||
// Arrange
|
||||
mockIndexingTechnique = IndexingType.ECONOMICAL
|
||||
render(<SegmentDetail {...defaultProps} isEditMode={true} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByTestId('keywords-input'), {
|
||||
target: { value: 'new,keywords' },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('keywords-input')).toHaveValue('new,keywords')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,442 @@
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { fireEvent, 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,
|
||||
onClickEdit,
|
||||
onDelete,
|
||||
onDeleteChildChunk,
|
||||
handleAddNewChildChunk,
|
||||
onClickSlice,
|
||||
archived,
|
||||
embeddingAvailable,
|
||||
focused,
|
||||
}: {
|
||||
detail: SegmentDetailModel
|
||||
onClick: () => void
|
||||
onChangeSwitch: (enabled: boolean, segId?: string) => Promise<void>
|
||||
onClickEdit: () => void
|
||||
onDelete: (segId: string) => Promise<void>
|
||||
onDeleteChildChunk: (segId: string, childChunkId: string) => Promise<void>
|
||||
handleAddNewChildChunk: (parentChunkId: string) => void
|
||||
onClickSlice: (childChunk: unknown) => 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>
|
||||
<button onClick={onClickEdit} data-testid="edit-btn">Edit</button>
|
||||
<button onClick={() => onChangeSwitch(true, detail.id)} data-testid="switch-btn">Switch</button>
|
||||
<button onClick={() => onDelete(detail.id)} data-testid="delete-btn">Delete</button>
|
||||
<button onClick={() => onDeleteChildChunk(detail.id, 'child-1')} data-testid="delete-child-btn">Delete Child</button>
|
||||
<button onClick={() => handleAddNewChildChunk(detail.id)} data-testid="add-child-btn">Add Child</button>
|
||||
<button onClick={() => onClickSlice({ id: 'slice-1' })} data-testid="click-slice-btn">Click Slice</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)
|
||||
})
|
||||
})
|
||||
|
||||
// Checkbox Selection
|
||||
describe('Checkbox Selection', () => {
|
||||
it('should render checkbox for each segment', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<SegmentList {...defaultProps} />)
|
||||
|
||||
// Assert - Checkbox component should exist
|
||||
const checkboxes = container.querySelectorAll('[class*="checkbox"]')
|
||||
expect(checkboxes.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should pass selectedSegmentIds to check state', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<SegmentList {...defaultProps} selectedSegmentIds={['seg-1']} />)
|
||||
|
||||
// Assert - component should render with selected state
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty selectedSegmentIds', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<SegmentList {...defaultProps} selectedSegmentIds={[]} />)
|
||||
|
||||
// Assert - component should render
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Card Actions
|
||||
describe('Card Actions', () => {
|
||||
it('should call onClick when card is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnClick = vi.fn()
|
||||
render(<SegmentList {...defaultProps} onClick={mockOnClick} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('card-click'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onChangeSwitch when switch button is clicked', async () => {
|
||||
// Arrange
|
||||
const mockOnChangeSwitch = vi.fn().mockResolvedValue(undefined)
|
||||
render(<SegmentList {...defaultProps} onChangeSwitch={mockOnChangeSwitch} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('switch-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnChangeSwitch).toHaveBeenCalledWith(true, 'seg-1')
|
||||
})
|
||||
|
||||
it('should call onDelete when delete button is clicked', async () => {
|
||||
// Arrange
|
||||
const mockOnDelete = vi.fn().mockResolvedValue(undefined)
|
||||
render(<SegmentList {...defaultProps} onDelete={mockOnDelete} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('delete-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnDelete).toHaveBeenCalledWith('seg-1')
|
||||
})
|
||||
|
||||
it('should call onDeleteChildChunk when delete child button is clicked', async () => {
|
||||
// Arrange
|
||||
const mockOnDeleteChildChunk = vi.fn().mockResolvedValue(undefined)
|
||||
render(<SegmentList {...defaultProps} onDeleteChildChunk={mockOnDeleteChildChunk} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('delete-child-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnDeleteChildChunk).toHaveBeenCalledWith('seg-1', 'child-1')
|
||||
})
|
||||
|
||||
it('should call handleAddNewChildChunk when add child button is clicked', () => {
|
||||
// Arrange
|
||||
const mockHandleAddNewChildChunk = vi.fn()
|
||||
render(<SegmentList {...defaultProps} handleAddNewChildChunk={mockHandleAddNewChildChunk} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('add-child-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockHandleAddNewChildChunk).toHaveBeenCalledWith('seg-1')
|
||||
})
|
||||
|
||||
it('should call onClickSlice when click slice button is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnClickSlice = vi.fn()
|
||||
render(<SegmentList {...defaultProps} onClickSlice={mockOnClickSlice} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('click-slice-btn'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnClickSlice).toHaveBeenCalledWith({ id: 'slice-1' })
|
||||
})
|
||||
|
||||
it('should call onClick with edit mode when edit button is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnClick = vi.fn()
|
||||
render(<SegmentList {...defaultProps} onClick={mockOnClick} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('edit-btn'))
|
||||
|
||||
// Assert - onClick is called from onClickEdit with isEditMode=true
|
||||
expect(mockOnClick).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,93 +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 the skeleton container', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
const skeletonContainer = container.firstChild
|
||||
expect(skeletonContainer).toHaveClass('flex', 'w-full', 'grow', 'flex-col')
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render 15 Slice components', () => {
|
||||
it('should render the correct number of slice elements', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
// Each Slice has a specific structure with gap-y-1
|
||||
const slices = container.querySelectorAll('.gap-y-1')
|
||||
expect(slices.length).toBe(15)
|
||||
// Assert - component renders 15 slices
|
||||
const sliceElements = container.querySelectorAll('.flex.flex-col.gap-y-1')
|
||||
expect(sliceElements).toHaveLength(15)
|
||||
})
|
||||
|
||||
it('should render mask overlay', () => {
|
||||
it('should render mask overlay element', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
const maskOverlay = container.querySelector('.bg-dataset-chunk-list-mask-bg')
|
||||
expect(maskOverlay).toBeInTheDocument()
|
||||
// Assert - check for the mask overlay element
|
||||
const maskElement = container.querySelector('.bg-dataset-chunk-list-mask-bg')
|
||||
expect(maskElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have overflow hidden', () => {
|
||||
it('should render with correct container classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
const skeletonContainer = container.firstChild
|
||||
expect(skeletonContainer).toHaveClass('overflow-y-hidden')
|
||||
// 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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Slice Component', () => {
|
||||
it('should render slice with correct structure', () => {
|
||||
// Structure tests
|
||||
describe('Structure', () => {
|
||||
it('should render slice elements with proper structure', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
// Each slice has two rows
|
||||
const sliceRows = container.querySelectorAll('.bg-state-base-hover')
|
||||
expect(sliceRows.length).toBeGreaterThan(0)
|
||||
// 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 label placeholder in each slice', () => {
|
||||
it('should render slice with width placeholder elements', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
// Label placeholder has specific width
|
||||
const labelPlaceholders = container.querySelectorAll('.w-\\[30px\\]')
|
||||
expect(labelPlaceholders.length).toBe(15) // One per slice
|
||||
// Assert - check for skeleton content width class
|
||||
const widthElements = container.querySelectorAll('.w-2\\/3')
|
||||
expect(widthElements.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render content placeholder in each slice', () => {
|
||||
it('should render slice elements with background classes', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
// Content placeholder has 2/3 width
|
||||
const contentPlaceholders = container.querySelectorAll('.w-2\\/3')
|
||||
expect(contentPlaceholders.length).toBe(15) // One per slice
|
||||
// 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 be memoized', () => {
|
||||
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 />)
|
||||
|
||||
const initialContent = container.innerHTML
|
||||
|
||||
// Rerender should produce same output
|
||||
// Act
|
||||
rerender(<FullDocListSkeleton />)
|
||||
rerender(<FullDocListSkeleton />)
|
||||
|
||||
expect(container.innerHTML).toBe(initialContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct z-index layering', () => {
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
const skeletonContainer = container.firstChild
|
||||
expect(skeletonContainer).toHaveClass('z-10')
|
||||
|
||||
const maskOverlay = container.querySelector('.z-20')
|
||||
expect(maskOverlay).toBeInTheDocument()
|
||||
// Assert
|
||||
const sliceElements = container.querySelectorAll('.flex.flex-col.gap-y-1')
|
||||
expect(sliceElements).toHaveLength(15)
|
||||
})
|
||||
|
||||
it('should have gap between slices', () => {
|
||||
it('should not have accessibility issues with skeleton content', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FullDocListSkeleton />)
|
||||
|
||||
const skeletonContainer = container.firstChild
|
||||
expect(skeletonContainer).toHaveClass('gap-y-3')
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -0,0 +1,545 @@
|
||||
import type { FullDocumentDetail } from '@/models/datasets'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Metadata, { FieldInfo } from './index'
|
||||
|
||||
// Mock document context
|
||||
vi.mock('../context', () => ({
|
||||
useDocumentContext: (selector: (state: { datasetId: string, documentId: string }) => unknown) => {
|
||||
return selector({ 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 modifyDocMetadata
|
||||
const mockModifyDocMetadata = vi.fn()
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
modifyDocMetadata: (...args: unknown[]) => mockModifyDocMetadata(...args),
|
||||
}))
|
||||
|
||||
// Mock useMetadataMap and related hooks
|
||||
vi.mock('@/hooks/use-metadata', () => ({
|
||||
useMetadataMap: () => ({
|
||||
book: {
|
||||
text: 'Book',
|
||||
iconName: 'book',
|
||||
subFieldsMap: {
|
||||
title: { label: 'Title', inputType: 'input' },
|
||||
language: { label: 'Language', inputType: 'select' },
|
||||
author: { label: 'Author', inputType: 'input' },
|
||||
publisher: { label: 'Publisher', inputType: 'input' },
|
||||
publication_date: { label: 'Publication Date', inputType: 'input' },
|
||||
isbn: { label: 'ISBN', inputType: 'input' },
|
||||
category: { label: 'Category', inputType: 'select' },
|
||||
},
|
||||
},
|
||||
web_page: {
|
||||
text: 'Web Page',
|
||||
iconName: 'web',
|
||||
subFieldsMap: {
|
||||
title: { label: 'Title', inputType: 'input' },
|
||||
url: { label: 'URL', inputType: 'input' },
|
||||
language: { label: 'Language', inputType: 'select' },
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
text: 'Paper',
|
||||
iconName: 'paper',
|
||||
subFieldsMap: {
|
||||
title: { label: 'Title', inputType: 'input' },
|
||||
language: { label: 'Language', inputType: 'select' },
|
||||
},
|
||||
},
|
||||
social_media_post: {
|
||||
text: 'Social Media Post',
|
||||
iconName: 'social',
|
||||
subFieldsMap: {
|
||||
platform: { label: 'Platform', inputType: 'input' },
|
||||
},
|
||||
},
|
||||
personal_document: {
|
||||
text: 'Personal Document',
|
||||
iconName: 'personal',
|
||||
subFieldsMap: {
|
||||
document_type: { label: 'Document Type', inputType: 'select' },
|
||||
},
|
||||
},
|
||||
business_document: {
|
||||
text: 'Business Document',
|
||||
iconName: 'business',
|
||||
subFieldsMap: {
|
||||
document_type: { label: 'Document Type', inputType: 'select' },
|
||||
},
|
||||
},
|
||||
im_chat_log: {
|
||||
text: 'IM Chat Log',
|
||||
iconName: 'chat',
|
||||
subFieldsMap: {
|
||||
platform: { label: 'Platform', inputType: 'input' },
|
||||
},
|
||||
},
|
||||
originInfo: {
|
||||
text: 'Origin Info',
|
||||
subFieldsMap: {
|
||||
data_source_type: { label: 'Data Source Type', inputType: 'input' },
|
||||
name: { label: 'Name', inputType: 'input' },
|
||||
},
|
||||
},
|
||||
technicalParameters: {
|
||||
text: 'Technical Parameters',
|
||||
subFieldsMap: {
|
||||
segment_count: { label: 'Segment Count', inputType: 'input' },
|
||||
hit_count: { label: 'Hit Count', inputType: 'input', render: (v: number, segCount?: number) => `${v}/${segCount}` },
|
||||
},
|
||||
},
|
||||
}),
|
||||
useLanguages: () => ({
|
||||
en: 'English',
|
||||
zh: 'Chinese',
|
||||
}),
|
||||
useBookCategories: () => ({
|
||||
'fiction': 'Fiction',
|
||||
'non-fiction': 'Non-Fiction',
|
||||
}),
|
||||
usePersonalDocCategories: () => ({
|
||||
resume: 'Resume',
|
||||
letter: 'Letter',
|
||||
}),
|
||||
useBusinessDocCategories: () => ({
|
||||
report: 'Report',
|
||||
proposal: 'Proposal',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock getTextWidthWithCanvas
|
||||
vi.mock('@/utils', () => ({
|
||||
asyncRunSafe: async (promise: Promise<unknown>) => {
|
||||
try {
|
||||
const result = await promise
|
||||
return [null, result]
|
||||
}
|
||||
catch (e) {
|
||||
return [e, null]
|
||||
}
|
||||
},
|
||||
getTextWidthWithCanvas: () => 100,
|
||||
}))
|
||||
|
||||
describe('Metadata', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const createMockDocDetail = (overrides = {}): FullDocumentDetail => ({
|
||||
id: 'doc-1',
|
||||
name: 'Test Document',
|
||||
doc_type: 'book',
|
||||
doc_metadata: {
|
||||
title: 'Test Book',
|
||||
author: 'Test Author',
|
||||
language: 'en',
|
||||
},
|
||||
data_source_type: 'upload_file',
|
||||
segment_count: 10,
|
||||
hit_count: 5,
|
||||
...overrides,
|
||||
} as FullDocumentDetail)
|
||||
|
||||
const defaultProps = {
|
||||
docDetail: createMockDocDetail(),
|
||||
loading: false,
|
||||
onUpdate: vi.fn(),
|
||||
}
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render metadata title', () => {
|
||||
// Arrange & Act
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/metadata\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render edit button', () => {
|
||||
// Arrange & Act
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/operation\.edit/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading state', () => {
|
||||
// Arrange & Act
|
||||
render(<Metadata {...defaultProps} loading={true} />)
|
||||
|
||||
// Assert - Loading component should be rendered
|
||||
expect(screen.queryByText(/metadata\.title/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display document type icon and text', () => {
|
||||
// Arrange & Act
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Book')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edit mode tests
|
||||
describe('Edit Mode', () => {
|
||||
it('should enter edit mode when edit button is clicked', () => {
|
||||
// Arrange
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/operation\.cancel/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/operation\.save/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show change link in edit mode', () => {
|
||||
// Arrange
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/operation\.change/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should cancel edit and restore values when cancel is clicked', () => {
|
||||
// Arrange
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.cancel/i))
|
||||
|
||||
// Assert - should be back to view mode
|
||||
expect(screen.getByText(/operation\.edit/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should save metadata when save button is clicked', async () => {
|
||||
// Arrange
|
||||
mockModifyDocMetadata.mockResolvedValueOnce({})
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.save/i))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockModifyDocMetadata).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show success notification after successful save', async () => {
|
||||
// Arrange
|
||||
mockModifyDocMetadata.mockResolvedValueOnce({})
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.save/i))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'success',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error notification after failed save', async () => {
|
||||
// Arrange
|
||||
mockModifyDocMetadata.mockRejectedValueOnce(new Error('Save failed'))
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.save/i))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'error',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Document type selection
|
||||
describe('Document Type Selection', () => {
|
||||
it('should show doc type selection when no doc_type exists', () => {
|
||||
// Arrange
|
||||
const docDetail = createMockDocDetail({ doc_type: '' })
|
||||
|
||||
// Act
|
||||
render(<Metadata {...defaultProps} docDetail={docDetail} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/metadata\.docTypeSelectTitle/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show description when no doc_type exists', () => {
|
||||
// Arrange
|
||||
const docDetail = createMockDocDetail({ doc_type: '' })
|
||||
|
||||
// Act
|
||||
render(<Metadata {...defaultProps} docDetail={docDetail} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/metadata\.desc/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show change link in edit mode when doc_type exists', () => {
|
||||
// Arrange
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/operation\.change/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show doc type change title after clicking change', () => {
|
||||
// Arrange
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(screen.getByText(/operation\.edit/i))
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText(/operation\.change/i))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/metadata\.docTypeChangeTitle/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Origin info and technical parameters
|
||||
describe('Fixed Fields', () => {
|
||||
it('should render origin info fields', () => {
|
||||
// Arrange & Act
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Assert - Origin info fields should be displayed
|
||||
expect(screen.getByText('Data Source Type')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render technical parameters fields', () => {
|
||||
// Arrange & Act
|
||||
render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Segment Count')).toBeInTheDocument()
|
||||
expect(screen.getByText('Hit Count')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle doc_type as others', () => {
|
||||
// Arrange
|
||||
const docDetail = createMockDocDetail({ doc_type: 'others' })
|
||||
|
||||
// Act
|
||||
const { container } = render(<Metadata {...defaultProps} docDetail={docDetail} />)
|
||||
|
||||
// Assert - should render without crashing
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined docDetail gracefully', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<Metadata {...defaultProps} docDetail={undefined} loading={false} />)
|
||||
|
||||
// Assert - should render without crashing
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update document type display when docDetail changes', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<Metadata {...defaultProps} />)
|
||||
|
||||
// Act - verify initial state shows Book
|
||||
expect(screen.getByText('Book')).toBeInTheDocument()
|
||||
|
||||
// Update with new doc type
|
||||
const updatedDocDetail = createMockDocDetail({ doc_type: 'paper' })
|
||||
rerender(<Metadata {...defaultProps} docDetail={updatedDocDetail} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Paper')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// First meta action button
|
||||
describe('First Meta Action Button', () => {
|
||||
it('should show first meta action button when no doc type exists', () => {
|
||||
// Arrange
|
||||
const docDetail = createMockDocDetail({ doc_type: '' })
|
||||
|
||||
// Act
|
||||
render(<Metadata {...defaultProps} docDetail={docDetail} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/metadata\.firstMetaAction/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// FieldInfo component tests
|
||||
describe('FieldInfo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const defaultFieldInfoProps = {
|
||||
label: 'Test Label',
|
||||
value: 'Test Value',
|
||||
displayedValue: 'Test Display Value',
|
||||
}
|
||||
|
||||
// Rendering
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
const { container } = render(<FieldInfo {...defaultFieldInfoProps} />)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render label', () => {
|
||||
// Arrange & Act
|
||||
render(<FieldInfo {...defaultFieldInfoProps} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render displayed value in view mode', () => {
|
||||
// Arrange & Act
|
||||
render(<FieldInfo {...defaultFieldInfoProps} showEdit={false} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Test Display Value')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Edit mode
|
||||
describe('Edit Mode', () => {
|
||||
it('should render input when showEdit is true and inputType is input', () => {
|
||||
// Arrange & Act
|
||||
render(<FieldInfo {...defaultFieldInfoProps} showEdit={true} inputType="input" onUpdate={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render select when showEdit is true and inputType is select', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<FieldInfo
|
||||
{...defaultFieldInfoProps}
|
||||
showEdit={true}
|
||||
inputType="select"
|
||||
selectOptions={[{ value: 'opt1', name: 'Option 1' }]}
|
||||
onUpdate={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - SimpleSelect should be rendered
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render textarea when showEdit is true and inputType is textarea', () => {
|
||||
// Arrange & Act
|
||||
render(<FieldInfo {...defaultFieldInfoProps} showEdit={true} inputType="textarea" onUpdate={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onUpdate when input value changes', () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = vi.fn()
|
||||
render(<FieldInfo {...defaultFieldInfoProps} showEdit={true} inputType="input" onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'New Value' } })
|
||||
|
||||
// Assert
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith('New Value')
|
||||
})
|
||||
|
||||
it('should call onUpdate when textarea value changes', () => {
|
||||
// Arrange
|
||||
const mockOnUpdate = vi.fn()
|
||||
render(<FieldInfo {...defaultFieldInfoProps} showEdit={true} inputType="textarea" onUpdate={mockOnUpdate} />)
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'New Textarea Value' } })
|
||||
|
||||
// Assert
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith('New Textarea Value')
|
||||
})
|
||||
})
|
||||
|
||||
// Props
|
||||
describe('Props', () => {
|
||||
it('should render value icon when provided', () => {
|
||||
// Arrange & Act
|
||||
render(<FieldInfo {...defaultFieldInfoProps} valueIcon={<span data-testid="value-icon">Icon</span>} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('value-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use defaultValue when provided', () => {
|
||||
// Arrange & Act
|
||||
render(<FieldInfo {...defaultFieldInfoProps} showEdit={true} inputType="input" defaultValue="Default" onUpdate={vi.fn()} />)
|
||||
|
||||
// Assert
|
||||
const input = screen.getByRole('textbox')
|
||||
expect(input).toHaveAttribute('placeholder')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,351 @@
|
||||
import type { ReactNode } from 'react'
|
||||
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: ReactNode, btnElement: 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
720
web/app/components/datasets/documents/index.spec.tsx
Normal file
720
web/app/components/datasets/documents/index.spec.tsx
Normal file
@ -0,0 +1,720 @@
|
||||
import type { DocumentListResponse } from '@/models/datasets'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { useDocumentList } from '@/service/knowledge/use-document'
|
||||
import useDocumentsPageState from './hooks/use-documents-page-state'
|
||||
import Documents from './index'
|
||||
|
||||
// Type for mock selector function - use `as MockState` to bypass strict type checking in tests
|
||||
type MockSelector = Parameters<typeof useDatasetDetailContextWithSelector>[0]
|
||||
type MockState = Parameters<MockSelector>[0]
|
||||
|
||||
// Mock Next.js router
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
replace: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
}),
|
||||
usePathname: () => '/datasets/test-dataset-id/documents',
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
}))
|
||||
|
||||
// Mock context providers
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: vi.fn((selector: (state: unknown) => unknown) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'test-dataset-id',
|
||||
name: 'Test Dataset',
|
||||
embedding_available: true,
|
||||
data_source_type: DataSourceType.FILE,
|
||||
runtime_mode: 'rag',
|
||||
},
|
||||
}
|
||||
return selector(mockState as MockState)
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(() => ({
|
||||
plan: { type: 'professional' },
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock document service hooks
|
||||
const mockInvalidDocumentList = vi.fn()
|
||||
const mockInvalidDocumentDetail = vi.fn()
|
||||
|
||||
vi.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentList: vi.fn(() => ({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: 'doc-1',
|
||||
name: 'Document 1',
|
||||
indexing_status: 'completed',
|
||||
data_source_type: 'upload_file',
|
||||
position: 1,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'doc-2',
|
||||
name: 'Document 2',
|
||||
indexing_status: 'indexing',
|
||||
data_source_type: 'upload_file',
|
||||
position: 2,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
has_more: false,
|
||||
} as DocumentListResponse,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
})),
|
||||
useInvalidDocumentList: vi.fn(() => mockInvalidDocumentList),
|
||||
useInvalidDocumentDetail: vi.fn(() => mockInvalidDocumentDetail),
|
||||
}))
|
||||
|
||||
// Mock segment service hooks
|
||||
vi.mock('@/service/knowledge/use-segment', () => ({
|
||||
useSegmentListKey: 'segment-list-key',
|
||||
useChildSegmentListKey: 'child-segment-list-key',
|
||||
}))
|
||||
|
||||
// Mock base service hooks
|
||||
vi.mock('@/service/use-base', () => ({
|
||||
useInvalid: vi.fn(() => vi.fn()),
|
||||
}))
|
||||
|
||||
// Mock metadata hook
|
||||
vi.mock('../metadata/hooks/use-edit-dataset-metadata', () => ({
|
||||
default: vi.fn(() => ({
|
||||
isShowEditModal: false,
|
||||
showEditModal: vi.fn(),
|
||||
hideEditModal: vi.fn(),
|
||||
datasetMetaData: [],
|
||||
handleAddMetaData: vi.fn(),
|
||||
handleRename: vi.fn(),
|
||||
handleDeleteMetaData: vi.fn(),
|
||||
builtInEnabled: false,
|
||||
setBuiltInEnabled: vi.fn(),
|
||||
builtInMetaData: [],
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock page state hook
|
||||
const mockSetSelectedIds = vi.fn()
|
||||
const mockHandleInputChange = vi.fn()
|
||||
const mockHandleStatusFilterChange = vi.fn()
|
||||
const mockHandleStatusFilterClear = vi.fn()
|
||||
const mockHandleSortChange = vi.fn()
|
||||
const mockHandlePageChange = vi.fn()
|
||||
const mockHandleLimitChange = vi.fn()
|
||||
const mockUpdatePollingState = vi.fn()
|
||||
const mockAdjustPageForTotal = vi.fn()
|
||||
|
||||
vi.mock('./hooks/use-documents-page-state', () => ({
|
||||
default: vi.fn(() => ({
|
||||
inputValue: '',
|
||||
searchValue: '',
|
||||
debouncedSearchValue: '',
|
||||
handleInputChange: mockHandleInputChange,
|
||||
statusFilterValue: 'all',
|
||||
sortValue: '-created_at' as const,
|
||||
normalizedStatusFilterValue: 'all',
|
||||
handleStatusFilterChange: mockHandleStatusFilterChange,
|
||||
handleStatusFilterClear: mockHandleStatusFilterClear,
|
||||
handleSortChange: mockHandleSortChange,
|
||||
currPage: 0,
|
||||
limit: 10,
|
||||
handlePageChange: mockHandlePageChange,
|
||||
handleLimitChange: mockHandleLimitChange,
|
||||
selectedIds: [] as string[],
|
||||
setSelectedIds: mockSetSelectedIds,
|
||||
timerCanRun: false,
|
||||
updatePollingState: mockUpdatePollingState,
|
||||
adjustPageForTotal: mockAdjustPageForTotal,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock child components - these have deep dependency chains (QueryClient, API hooks, contexts)
|
||||
// Mocking them allows us to test the Documents component logic in isolation
|
||||
vi.mock('./components/documents-header', () => ({
|
||||
default: ({
|
||||
datasetId,
|
||||
embeddingAvailable,
|
||||
onInputChange,
|
||||
onAddDocument,
|
||||
onStatusFilterChange,
|
||||
onStatusFilterClear,
|
||||
onSortChange,
|
||||
}: {
|
||||
datasetId: string
|
||||
dataSourceType?: string
|
||||
embeddingAvailable: boolean
|
||||
isFreePlan: boolean
|
||||
statusFilterValue: string
|
||||
sortValue: string
|
||||
inputValue: string
|
||||
onInputChange: (value: string) => void
|
||||
onAddDocument: () => void
|
||||
onStatusFilterChange: (value: string) => void
|
||||
onStatusFilterClear: () => void
|
||||
onSortChange: (value: string) => void
|
||||
isShowEditMetadataModal: boolean
|
||||
showEditMetadataModal: () => void
|
||||
hideEditMetadataModal: () => void
|
||||
datasetMetaData: unknown[]
|
||||
builtInMetaData: unknown[]
|
||||
builtInEnabled: boolean
|
||||
onAddMetaData: () => void
|
||||
onRenameMetaData: () => void
|
||||
onDeleteMetaData: () => void
|
||||
onBuiltInEnabledChange: () => void
|
||||
}) => (
|
||||
<div data-testid="documents-header">
|
||||
<span data-testid="header-dataset-id">{datasetId}</span>
|
||||
<span data-testid="header-embedding-available">{String(embeddingAvailable)}</span>
|
||||
<input
|
||||
data-testid="search-input"
|
||||
onChange={e => onInputChange(e.target.value)}
|
||||
placeholder="Search documents"
|
||||
/>
|
||||
<button data-testid="add-document-btn" onClick={onAddDocument}>
|
||||
Add Document
|
||||
</button>
|
||||
<button data-testid="status-filter-btn" onClick={() => onStatusFilterChange('completed')}>
|
||||
Filter Status
|
||||
</button>
|
||||
<button data-testid="clear-filter-btn" onClick={onStatusFilterClear}>
|
||||
Clear Filter
|
||||
</button>
|
||||
<button data-testid="sort-btn" onClick={() => onSortChange('-updated_at')}>
|
||||
Sort
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./components/empty-element', () => ({
|
||||
default: ({ canAdd, onClick, type }: {
|
||||
canAdd: boolean
|
||||
onClick: () => void
|
||||
type: 'sync' | 'upload'
|
||||
}) => (
|
||||
<div data-testid="empty-element">
|
||||
<span data-testid="empty-can-add">{String(canAdd)}</span>
|
||||
<span data-testid="empty-type">{type}</span>
|
||||
<button data-testid="empty-add-btn" onClick={onClick}>
|
||||
Add Document
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./components/list', () => ({
|
||||
default: ({
|
||||
documents,
|
||||
datasetId,
|
||||
onUpdate,
|
||||
selectedIds,
|
||||
onSelectedIdChange,
|
||||
pagination,
|
||||
}: {
|
||||
embeddingAvailable: boolean
|
||||
documents: unknown[]
|
||||
datasetId: string
|
||||
onUpdate: () => void
|
||||
selectedIds: string[]
|
||||
onSelectedIdChange: (ids: string[]) => void
|
||||
statusFilterValue: string
|
||||
remoteSortValue: string
|
||||
pagination: {
|
||||
total: number
|
||||
limit: number
|
||||
current: number
|
||||
onChange: (page: number) => void
|
||||
onLimitChange: (limit: number) => void
|
||||
}
|
||||
onManageMetadata: () => void
|
||||
}) => (
|
||||
<div data-testid="documents-list">
|
||||
<span data-testid="list-dataset-id">{datasetId}</span>
|
||||
<span data-testid="list-documents-count">{documents.length}</span>
|
||||
<span data-testid="list-selected-count">{selectedIds.length}</span>
|
||||
<span data-testid="list-total">{pagination.total}</span>
|
||||
<span data-testid="list-current-page">{pagination.current}</span>
|
||||
<button data-testid="update-btn" onClick={onUpdate}>
|
||||
Update
|
||||
</button>
|
||||
<button data-testid="select-btn" onClick={() => onSelectedIdChange(['doc-1'])}>
|
||||
Select Doc
|
||||
</button>
|
||||
<button data-testid="page-change-btn" onClick={() => pagination.onChange(1)}>
|
||||
Next Page
|
||||
</button>
|
||||
<button data-testid="limit-change-btn" onClick={() => pagination.onLimitChange(20)}>
|
||||
Change Limit
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Documents', () => {
|
||||
const defaultProps = {
|
||||
datasetId: 'test-dataset-id',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPush.mockClear()
|
||||
// Reset context mocks to default
|
||||
vi.mocked(useDatasetDetailContextWithSelector).mockImplementation((selector: MockSelector) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'test-dataset-id',
|
||||
name: 'Test Dataset',
|
||||
embedding_available: true,
|
||||
data_source_type: DataSourceType.FILE,
|
||||
runtime_mode: 'rag',
|
||||
},
|
||||
}
|
||||
return selector(mockState as MockState)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('documents-header')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render DocumentsHeader with correct props', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('header-dataset-id')).toHaveTextContent('test-dataset-id')
|
||||
expect(screen.getByTestId('header-embedding-available')).toHaveTextContent('true')
|
||||
})
|
||||
|
||||
it('should render document list when documents exist', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('documents-list')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('list-documents-count')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('should render loading state when isLoading is true', () => {
|
||||
vi.mocked(useDocumentList).mockReturnValueOnce({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDocumentList>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.queryByTestId('documents-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render empty element when no documents exist', () => {
|
||||
vi.mocked(useDocumentList).mockReturnValueOnce({
|
||||
data: { data: [], total: 0, page: 1, limit: 10, has_more: false },
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDocumentList>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('empty-element')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('empty-can-add')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('empty-type')).toHaveTextContent('upload')
|
||||
})
|
||||
|
||||
it('should render sync type empty element for Notion data source', () => {
|
||||
vi.mocked(useDatasetDetailContextWithSelector).mockImplementation((selector: MockSelector) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'test-dataset-id',
|
||||
name: 'Test Dataset',
|
||||
embedding_available: true,
|
||||
data_source_type: DataSourceType.NOTION,
|
||||
runtime_mode: 'rag',
|
||||
},
|
||||
}
|
||||
return selector(mockState as MockState)
|
||||
})
|
||||
vi.mocked(useDocumentList).mockReturnValueOnce({
|
||||
data: { data: [], total: 0, page: 1, limit: 10, has_more: false },
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDocumentList>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('empty-type')).toHaveTextContent('sync')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should pass datasetId to child components', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('header-dataset-id')).toHaveTextContent('test-dataset-id')
|
||||
})
|
||||
|
||||
it('should handle different datasetId', () => {
|
||||
render(<Documents datasetId="different-dataset-id" />)
|
||||
expect(screen.getByTestId('header-dataset-id')).toHaveTextContent('different-dataset-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call handleInputChange when search input changes', async () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
const searchInput = screen.getByTestId('search-input')
|
||||
fireEvent.change(searchInput, { target: { value: 'test' } })
|
||||
|
||||
expect(mockHandleInputChange).toHaveBeenCalledWith('test')
|
||||
})
|
||||
|
||||
it('should call handleStatusFilterChange when filter button is clicked', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('status-filter-btn').click()
|
||||
|
||||
expect(mockHandleStatusFilterChange).toHaveBeenCalledWith('completed')
|
||||
})
|
||||
|
||||
it('should call handleStatusFilterClear when clear button is clicked', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('clear-filter-btn').click()
|
||||
|
||||
expect(mockHandleStatusFilterClear).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call handleSortChange when sort button is clicked', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('sort-btn').click()
|
||||
|
||||
expect(mockHandleSortChange).toHaveBeenCalledWith('-updated_at')
|
||||
})
|
||||
|
||||
it('should call setSelectedIds when document is selected', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('select-btn').click()
|
||||
|
||||
expect(mockSetSelectedIds).toHaveBeenCalledWith(['doc-1'])
|
||||
})
|
||||
|
||||
it('should call handlePageChange when page changes', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('page-change-btn').click()
|
||||
|
||||
expect(mockHandlePageChange).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('should call handleLimitChange when limit changes', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('limit-change-btn').click()
|
||||
|
||||
expect(mockHandleLimitChange).toHaveBeenCalledWith(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Router Navigation', () => {
|
||||
it('should navigate to create page when add document is clicked', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('add-document-btn').click()
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-id/documents/create')
|
||||
})
|
||||
|
||||
it('should navigate to pipeline create page when dataset is rag_pipeline mode', () => {
|
||||
vi.mocked(useDatasetDetailContextWithSelector).mockImplementation((selector: MockSelector) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'test-dataset-id',
|
||||
name: 'Test Dataset',
|
||||
embedding_available: true,
|
||||
data_source_type: DataSourceType.FILE,
|
||||
runtime_mode: 'rag_pipeline',
|
||||
},
|
||||
}
|
||||
return selector(mockState as MockState)
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('add-document-btn').click()
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-id/documents/create-from-pipeline')
|
||||
})
|
||||
|
||||
it('should navigate from empty element add button', () => {
|
||||
vi.mocked(useDatasetDetailContextWithSelector).mockImplementation((selector: MockSelector) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'test-dataset-id',
|
||||
name: 'Test Dataset',
|
||||
embedding_available: true,
|
||||
data_source_type: DataSourceType.FILE,
|
||||
runtime_mode: 'rag',
|
||||
},
|
||||
}
|
||||
return selector(mockState as MockState)
|
||||
})
|
||||
vi.mocked(useDocumentList).mockReturnValueOnce({
|
||||
data: { data: [], total: 0, page: 1, limit: 10, has_more: false },
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDocumentList>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('empty-add-btn').click()
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-id/documents/create')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Side Effects and Cleanup', () => {
|
||||
it('should call updatePollingState when documents response changes', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(mockUpdatePollingState).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call adjustPageForTotal when documents response changes', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(mockAdjustPageForTotal).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Callback Stability and Memoization', () => {
|
||||
it('should call handleUpdate with invalidation functions', async () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
screen.getByTestId('update-btn').click()
|
||||
|
||||
expect(mockInvalidDocumentList).toHaveBeenCalled()
|
||||
expect(mockInvalidDocumentDetail).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle update with delayed chunk invalidation', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
screen.getByTestId('update-btn').click()
|
||||
|
||||
expect(mockInvalidDocumentList).toHaveBeenCalled()
|
||||
expect(mockInvalidDocumentDetail).toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(5000)
|
||||
})
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
it('should handle undefined dataset gracefully', () => {
|
||||
vi.mocked(useDatasetDetailContextWithSelector).mockImplementation((selector: MockSelector) => {
|
||||
const mockState = { dataset: undefined }
|
||||
return selector(mockState as MockState)
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('documents-header')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty documents array', () => {
|
||||
vi.mocked(useDocumentList).mockReturnValueOnce({
|
||||
data: { data: [], total: 0, page: 1, limit: 10, has_more: false },
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDocumentList>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('empty-element')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined documentsRes', () => {
|
||||
vi.mocked(useDocumentList).mockReturnValueOnce({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useDocumentList>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('empty-element')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle embedding not available', () => {
|
||||
vi.mocked(useDatasetDetailContextWithSelector).mockImplementation((selector: MockSelector) => {
|
||||
const mockState = {
|
||||
dataset: {
|
||||
id: 'test-dataset-id',
|
||||
name: 'Test Dataset',
|
||||
embedding_available: false,
|
||||
data_source_type: DataSourceType.FILE,
|
||||
runtime_mode: 'rag',
|
||||
},
|
||||
}
|
||||
return selector(mockState as MockState)
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('header-embedding-available')).toHaveTextContent('false')
|
||||
})
|
||||
|
||||
it('should handle free plan user', () => {
|
||||
vi.mocked(useProviderContext).mockReturnValueOnce({
|
||||
plan: { type: 'sandbox' },
|
||||
} as ReturnType<typeof useProviderContext>)
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('documents-header')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Polling State', () => {
|
||||
it('should enable polling when documents are indexing', () => {
|
||||
vi.mocked(useDocumentsPageState).mockReturnValueOnce({
|
||||
inputValue: '',
|
||||
searchValue: '',
|
||||
debouncedSearchValue: '',
|
||||
handleInputChange: mockHandleInputChange,
|
||||
statusFilterValue: 'all',
|
||||
sortValue: '-created_at' as const,
|
||||
normalizedStatusFilterValue: 'all',
|
||||
handleStatusFilterChange: mockHandleStatusFilterChange,
|
||||
handleStatusFilterClear: mockHandleStatusFilterClear,
|
||||
handleSortChange: mockHandleSortChange,
|
||||
currPage: 0,
|
||||
limit: 10,
|
||||
handlePageChange: mockHandlePageChange,
|
||||
handleLimitChange: mockHandleLimitChange,
|
||||
selectedIds: [] as string[],
|
||||
setSelectedIds: mockSetSelectedIds,
|
||||
timerCanRun: true,
|
||||
updatePollingState: mockUpdatePollingState,
|
||||
adjustPageForTotal: mockAdjustPageForTotal,
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('documents-list')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('should display correct total in list', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('list-total')).toHaveTextContent('2')
|
||||
})
|
||||
|
||||
it('should display correct current page', () => {
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('list-current-page')).toHaveTextContent('0')
|
||||
})
|
||||
|
||||
it('should handle page changes', () => {
|
||||
vi.mocked(useDocumentsPageState).mockReturnValueOnce({
|
||||
inputValue: '',
|
||||
searchValue: '',
|
||||
debouncedSearchValue: '',
|
||||
handleInputChange: mockHandleInputChange,
|
||||
statusFilterValue: 'all',
|
||||
sortValue: '-created_at' as const,
|
||||
normalizedStatusFilterValue: 'all',
|
||||
handleStatusFilterChange: mockHandleStatusFilterChange,
|
||||
handleStatusFilterClear: mockHandleStatusFilterClear,
|
||||
handleSortChange: mockHandleSortChange,
|
||||
currPage: 2,
|
||||
limit: 10,
|
||||
handlePageChange: mockHandlePageChange,
|
||||
handleLimitChange: mockHandleLimitChange,
|
||||
selectedIds: [] as string[],
|
||||
setSelectedIds: mockSetSelectedIds,
|
||||
timerCanRun: false,
|
||||
updatePollingState: mockUpdatePollingState,
|
||||
adjustPageForTotal: mockAdjustPageForTotal,
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('list-current-page')).toHaveTextContent('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Selection State', () => {
|
||||
it('should display selected count', () => {
|
||||
vi.mocked(useDocumentsPageState).mockReturnValueOnce({
|
||||
inputValue: '',
|
||||
searchValue: '',
|
||||
debouncedSearchValue: '',
|
||||
handleInputChange: mockHandleInputChange,
|
||||
statusFilterValue: 'all',
|
||||
sortValue: '-created_at' as const,
|
||||
normalizedStatusFilterValue: 'all',
|
||||
handleStatusFilterChange: mockHandleStatusFilterChange,
|
||||
handleStatusFilterClear: mockHandleStatusFilterClear,
|
||||
handleSortChange: mockHandleSortChange,
|
||||
currPage: 0,
|
||||
limit: 10,
|
||||
handlePageChange: mockHandlePageChange,
|
||||
handleLimitChange: mockHandleLimitChange,
|
||||
selectedIds: ['doc-1', 'doc-2'],
|
||||
setSelectedIds: mockSetSelectedIds,
|
||||
timerCanRun: false,
|
||||
updatePollingState: mockUpdatePollingState,
|
||||
adjustPageForTotal: mockAdjustPageForTotal,
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('list-selected-count')).toHaveTextContent('2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filter and Sort State', () => {
|
||||
it('should pass filter value to list', () => {
|
||||
vi.mocked(useDocumentsPageState).mockReturnValueOnce({
|
||||
inputValue: 'test search',
|
||||
searchValue: 'test search',
|
||||
debouncedSearchValue: 'test search',
|
||||
handleInputChange: mockHandleInputChange,
|
||||
statusFilterValue: 'completed',
|
||||
sortValue: '-created_at' as const,
|
||||
normalizedStatusFilterValue: 'completed',
|
||||
handleStatusFilterChange: mockHandleStatusFilterChange,
|
||||
handleStatusFilterClear: mockHandleStatusFilterClear,
|
||||
handleSortChange: mockHandleSortChange,
|
||||
currPage: 0,
|
||||
limit: 10,
|
||||
handlePageChange: mockHandlePageChange,
|
||||
handleLimitChange: mockHandleLimitChange,
|
||||
selectedIds: [] as string[],
|
||||
setSelectedIds: mockSetSelectedIds,
|
||||
timerCanRun: false,
|
||||
updatePollingState: mockUpdatePollingState,
|
||||
adjustPageForTotal: mockAdjustPageForTotal,
|
||||
})
|
||||
|
||||
render(<Documents {...defaultProps} />)
|
||||
expect(screen.getByTestId('documents-list')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user