mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +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,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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user