test: add comprehensive unit and integration tests for dataset module (#32187)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Coding On Star
2026-02-12 10:00:32 +08:00
committed by GitHub
parent f953331f91
commit 10f85074e8
388 changed files with 22637 additions and 15567 deletions

View File

@ -0,0 +1,49 @@
import type { DocumentItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import DocumentList from '../document-list'
vi.mock('../../document-file-icon', () => ({
default: ({ name, extension }: { name?: string, extension?: string }) => (
<span data-testid="file-icon">
{name}
.
{extension}
</span>
),
}))
describe('DocumentList', () => {
const mockList = [
{ id: 'doc-1', name: 'report', extension: 'pdf' },
{ id: 'doc-2', name: 'data', extension: 'csv' },
] as DocumentItem[]
const onChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it('should render all documents', () => {
render(<DocumentList list={mockList} onChange={onChange} />)
expect(screen.getByText('report')).toBeInTheDocument()
expect(screen.getByText('data')).toBeInTheDocument()
})
it('should render file icons', () => {
render(<DocumentList list={mockList} onChange={onChange} />)
expect(screen.getAllByTestId('file-icon')).toHaveLength(2)
})
it('should call onChange with document on click', () => {
render(<DocumentList list={mockList} onChange={onChange} />)
fireEvent.click(screen.getByText('report'))
expect(onChange).toHaveBeenCalledWith(mockList[0])
})
it('should render empty list without errors', () => {
const { container } = render(<DocumentList list={[]} onChange={onChange} />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { ChunkingMode, DataSourceType } from '@/models/datasets'
import DocumentPicker from './index'
import DocumentPicker from '../index'
// Mock portal-to-follow-elem - always render content for testing
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
@ -52,25 +52,6 @@ vi.mock('@/service/knowledge/use-document', () => ({
useDocumentList: mockUseDocumentList,
}))
// Mock icons - mock all remixicon components used in the component tree
vi.mock('@remixicon/react', () => ({
RiArrowDownSLine: () => <span data-testid="arrow-icon"></span>,
RiFile3Fill: () => <span data-testid="file-icon">📄</span>,
RiFileCodeFill: () => <span data-testid="file-code-icon">📄</span>,
RiFileExcelFill: () => <span data-testid="file-excel-icon">📄</span>,
RiFileGifFill: () => <span data-testid="file-gif-icon">📄</span>,
RiFileImageFill: () => <span data-testid="file-image-icon">📄</span>,
RiFileMusicFill: () => <span data-testid="file-music-icon">📄</span>,
RiFilePdf2Fill: () => <span data-testid="file-pdf-icon">📄</span>,
RiFilePpt2Fill: () => <span data-testid="file-ppt-icon">📄</span>,
RiFileTextFill: () => <span data-testid="file-text-icon">📄</span>,
RiFileVideoFill: () => <span data-testid="file-video-icon">📄</span>,
RiFileWordFill: () => <span data-testid="file-word-icon">📄</span>,
RiMarkdownFill: () => <span data-testid="file-markdown-icon">📄</span>,
RiSearchLine: () => <span data-testid="search-icon">🔍</span>,
RiCloseLine: () => <span data-testid="close-icon"></span>,
}))
// Factory function to create mock SimpleDocumentDetail
const createMockDocument = (overrides: Partial<SimpleDocumentDetail> = {}): SimpleDocumentDetail => ({
id: `doc-${Math.random().toString(36).substr(2, 9)}`,
@ -211,12 +192,6 @@ describe('DocumentPicker', () => {
expect(screen.getByText('--')).toBeInTheDocument()
})
it('should render arrow icon', () => {
renderComponent()
expect(screen.getByTestId('arrow-icon')).toBeInTheDocument()
})
it('should render general mode label', () => {
renderComponent({
value: {
@ -473,7 +448,7 @@ describe('DocumentPicker', () => {
describe('Memoization Logic', () => {
it('should be wrapped with React.memo', () => {
// React.memo components have a $$typeof property
expect((DocumentPicker as any).$$typeof).toBeDefined()
expect((DocumentPicker as unknown as { $$typeof: symbol }).$$typeof).toBeDefined()
})
it('should compute parentModeLabel correctly with useMemo', () => {
@ -952,7 +927,6 @@ describe('DocumentPicker', () => {
renderComponent({ onChange })
// Click on a document in the list
fireEvent.click(screen.getByText('Document 2'))
// handleChange should find the document and call onChange with full document
@ -1026,8 +1000,9 @@ describe('DocumentPicker', () => {
},
})
// FileIcon should be rendered via DocumentFileIcon - pdf renders pdf icon
expect(screen.getByTestId('file-pdf-icon')).toBeInTheDocument()
// FileIcon should render an SVG icon for the file extension
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})

View File

@ -1,20 +1,7 @@
import type { DocumentItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import PreviewDocumentPicker from './preview-document-picker'
// Override shared i18n mock for custom translations
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, params?: Record<string, unknown>) => {
if (key === 'preprocessDocument' && params?.num)
return `${params.num} files`
const prefix = params?.ns ? `${params.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
import PreviewDocumentPicker from '../preview-document-picker'
// Mock portal-to-follow-elem - always render content for testing
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
@ -45,23 +32,6 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
),
}))
// Mock icons
vi.mock('@remixicon/react', () => ({
RiArrowDownSLine: () => <span data-testid="arrow-icon"></span>,
RiFile3Fill: () => <span data-testid="file-icon">📄</span>,
RiFileCodeFill: () => <span data-testid="file-code-icon">📄</span>,
RiFileExcelFill: () => <span data-testid="file-excel-icon">📄</span>,
RiFileGifFill: () => <span data-testid="file-gif-icon">📄</span>,
RiFileImageFill: () => <span data-testid="file-image-icon">📄</span>,
RiFileMusicFill: () => <span data-testid="file-music-icon">📄</span>,
RiFilePdf2Fill: () => <span data-testid="file-pdf-icon">📄</span>,
RiFilePpt2Fill: () => <span data-testid="file-ppt-icon">📄</span>,
RiFileTextFill: () => <span data-testid="file-text-icon">📄</span>,
RiFileVideoFill: () => <span data-testid="file-video-icon">📄</span>,
RiFileWordFill: () => <span data-testid="file-word-icon">📄</span>,
RiMarkdownFill: () => <span data-testid="file-markdown-icon">📄</span>,
}))
// Factory function to create mock DocumentItem
const createMockDocumentItem = (overrides: Partial<DocumentItem> = {}): DocumentItem => ({
id: `doc-${Math.random().toString(36).substr(2, 9)}`,
@ -134,19 +104,14 @@ describe('PreviewDocumentPicker', () => {
expect(screen.getByText('--')).toBeInTheDocument()
})
it('should render arrow icon', () => {
renderComponent()
expect(screen.getByTestId('arrow-icon')).toBeInTheDocument()
})
it('should render file icon', () => {
renderComponent({
value: createMockDocumentItem({ extension: 'txt' }),
files: [], // Use empty files to avoid duplicate icons
})
expect(screen.getByTestId('file-text-icon')).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
it('should render pdf icon for pdf extension', () => {
@ -155,7 +120,8 @@ describe('PreviewDocumentPicker', () => {
files: [], // Use empty files to avoid duplicate icons
})
expect(screen.getByTestId('file-pdf-icon')).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})
@ -206,7 +172,8 @@ describe('PreviewDocumentPicker', () => {
value: createMockDocumentItem({ name: 'test.docx', extension: 'docx' }),
})
expect(screen.getByTestId('file-word-icon')).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})
@ -282,7 +249,7 @@ describe('PreviewDocumentPicker', () => {
// Tests for component memoization
describe('Component Memoization', () => {
it('should be wrapped with React.memo', () => {
expect((PreviewDocumentPicker as any).$$typeof).toBeDefined()
expect((PreviewDocumentPicker as unknown as { $$typeof: symbol }).$$typeof).toBeDefined()
})
it('should not re-render when props are the same', () => {
@ -329,7 +296,6 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files, onChange })
// Click on a document
fireEvent.click(screen.getByText('Document 2'))
// handleChange should call onChange with the selected item
@ -506,21 +472,16 @@ describe('PreviewDocumentPicker', () => {
})
describe('extension variations', () => {
const extensions = [
{ ext: 'txt', icon: 'file-text-icon' },
{ ext: 'pdf', icon: 'file-pdf-icon' },
{ ext: 'docx', icon: 'file-word-icon' },
{ ext: 'xlsx', icon: 'file-excel-icon' },
{ ext: 'md', icon: 'file-markdown-icon' },
]
const extensions = ['txt', 'pdf', 'docx', 'xlsx', 'md']
it.each(extensions)('should render correct icon for $ext extension', ({ ext, icon }) => {
it.each(extensions)('should render icon for %s extension', (ext) => {
renderComponent({
value: createMockDocumentItem({ extension: ext }),
files: [], // Use empty files to avoid duplicate icons
})
expect(screen.getByTestId(icon)).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})
})
@ -543,7 +504,6 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files, onChange })
// Click on first document
fireEvent.click(screen.getByText('Document 1'))
expect(onChange).toHaveBeenCalledWith(files[0])
@ -568,7 +528,7 @@ describe('PreviewDocumentPicker', () => {
onChange={vi.fn()}
/>,
)
expect(screen.getByText('3 files')).toBeInTheDocument()
expect(screen.getByText(/dataset\.preprocessDocument/)).toBeInTheDocument()
})
})
@ -609,7 +569,6 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files, onChange })
// Click first document
fireEvent.click(screen.getByText('Document 1'))
expect(onChange).toHaveBeenCalledWith(files[0])
@ -624,11 +583,9 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files: customFiles, onChange })
// Click on first custom file
fireEvent.click(screen.getByText('Custom File 1'))
expect(onChange).toHaveBeenCalledWith(customFiles[0])
// Click on second custom file
fireEvent.click(screen.getByText('Custom File 2'))
expect(onChange).toHaveBeenCalledWith(customFiles[1])
})