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:
Coding On Star
2026-01-27 15:43:27 +08:00
committed by GitHub
parent eca26a9b9b
commit c8abe1c306
105 changed files with 28225 additions and 686 deletions

View File

@ -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(['问题', '答案'])
})
})
})

View File

@ -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()
})
})
})
})

View File

@ -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()
})
})
})