chore: enhance Jest setup and add new tests for dataset creation components (#29825)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
This commit is contained in:
@@ -0,0 +1,777 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import EmptyDatasetCreationModal from './index'
|
||||
import { createEmptyDataset } from '@/service/datasets'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
|
||||
// Mock Next.js router
|
||||
const mockPush = jest.fn()
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock createEmptyDataset API
|
||||
jest.mock('@/service/datasets', () => ({
|
||||
createEmptyDataset: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock useInvalidDatasetList hook
|
||||
jest.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useInvalidDatasetList: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock ToastContext - need to mock both createContext and useContext from use-context-selector
|
||||
const mockNotify = jest.fn()
|
||||
jest.mock('use-context-selector', () => ({
|
||||
createContext: jest.fn(() => ({
|
||||
Provider: ({ children }: { children: React.ReactNode }) => children,
|
||||
})),
|
||||
useContext: jest.fn(() => ({ notify: mockNotify })),
|
||||
}))
|
||||
|
||||
// Type cast mocked functions
|
||||
const mockCreateEmptyDataset = createEmptyDataset as jest.MockedFunction<typeof createEmptyDataset>
|
||||
const mockInvalidDatasetList = jest.fn()
|
||||
const mockUseInvalidDatasetList = useInvalidDatasetList as jest.MockedFunction<typeof useInvalidDatasetList>
|
||||
|
||||
// Test data builder for props
|
||||
const createDefaultProps = (overrides?: Partial<{ show: boolean; onHide: () => void }>) => ({
|
||||
show: true,
|
||||
onHide: jest.fn(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('EmptyDatasetCreationModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockUseInvalidDatasetList.mockReturnValue(mockInvalidDatasetList)
|
||||
mockCreateEmptyDataset.mockResolvedValue({
|
||||
id: 'dataset-123',
|
||||
name: 'Test Dataset',
|
||||
} as ReturnType<typeof createEmptyDataset> extends Promise<infer T> ? T : never)
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests - Verify component renders correctly
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing when show is true', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert - Check modal title is rendered
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal with correct elements', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.tip')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.input')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.confirmButton')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.cancelButton')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render input with empty value initially', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
expect(input.value).toBe('')
|
||||
})
|
||||
|
||||
it('should not render modal content when show is false', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ show: false })
|
||||
|
||||
// Act
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
|
||||
// Assert - Modal should not be visible (check for absence of title)
|
||||
expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing - Verify all prop variations work correctly
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('show prop', () => {
|
||||
it('should show modal when show is true', () => {
|
||||
// Arrange & Act
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide modal when show is false', () => {
|
||||
// Arrange & Act
|
||||
render(<EmptyDatasetCreationModal show={false} onHide={jest.fn()} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should toggle visibility when show prop changes', () => {
|
||||
// Arrange
|
||||
const onHide = jest.fn()
|
||||
const { rerender } = render(<EmptyDatasetCreationModal show={false} onHide={onHide} />)
|
||||
|
||||
// Act & Assert - Initially hidden
|
||||
expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument()
|
||||
|
||||
// Act & Assert - Show modal
|
||||
rerender(<EmptyDatasetCreationModal show={true} onHide={onHide} />)
|
||||
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onHide prop', () => {
|
||||
it('should call onHide when cancel button is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
|
||||
// Act
|
||||
const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onHide when close icon is clicked', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
|
||||
// Act - Wait for modal to be rendered, then find the close span
|
||||
// The close span is located in the modalHeader div, next to the title
|
||||
const titleElement = await screen.findByText('datasetCreation.stepOne.modal.title')
|
||||
const headerDiv = titleElement.parentElement
|
||||
const closeButton = headerDiv?.querySelector('span')
|
||||
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
// Assert
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// State Management - Test input state updates
|
||||
// ==========================================
|
||||
describe('State Management', () => {
|
||||
it('should update input value when user types', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'My Dataset' } })
|
||||
|
||||
// Assert
|
||||
expect(input.value).toBe('My Dataset')
|
||||
})
|
||||
|
||||
it('should persist input value when modal is hidden and shown again via rerender', () => {
|
||||
// Arrange
|
||||
const onHide = jest.fn()
|
||||
const { rerender } = render(<EmptyDatasetCreationModal show={true} onHide={onHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
|
||||
// Act - Type in input
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
expect(input.value).toBe('Test Dataset')
|
||||
|
||||
// Hide and show modal via rerender (component is not unmounted, state persists)
|
||||
rerender(<EmptyDatasetCreationModal show={false} onHide={onHide} />)
|
||||
rerender(<EmptyDatasetCreationModal show={true} onHide={onHide} />)
|
||||
|
||||
// Assert - Input value persists because component state is preserved during rerender
|
||||
const newInput = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
expect(newInput.value).toBe('Test Dataset')
|
||||
})
|
||||
|
||||
it('should handle consecutive input changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
|
||||
|
||||
// Act & Assert
|
||||
fireEvent.change(input, { target: { value: 'A' } })
|
||||
expect(input.value).toBe('A')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'AB' } })
|
||||
expect(input.value).toBe('AB')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'ABC' } })
|
||||
expect(input.value).toBe('ABC')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions - Test event handlers
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should submit form when confirm button is clicked with valid input', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Valid Dataset Name' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Valid Dataset Name' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error notification when input is empty', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Click confirm without entering a name
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.nameNotEmpty',
|
||||
})
|
||||
})
|
||||
expect(mockCreateEmptyDataset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error notification when input exceeds 40 characters', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Enter a name longer than 40 characters
|
||||
const longName = 'A'.repeat(41)
|
||||
fireEvent.change(input, { target: { value: longName } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.nameLengthInvalid',
|
||||
})
|
||||
})
|
||||
expect(mockCreateEmptyDataset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow exactly 40 characters', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Enter exactly 40 characters
|
||||
const exactLengthName = 'A'.repeat(40)
|
||||
fireEvent.change(input, { target: { value: exactLengthName } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: exactLengthName })
|
||||
})
|
||||
})
|
||||
|
||||
it('should close modal on cancel button click', () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton')
|
||||
|
||||
// Act
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// API Calls - Test API interactions
|
||||
// ==========================================
|
||||
describe('API Calls', () => {
|
||||
it('should call createEmptyDataset with correct parameters', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'New Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'New Dataset' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should call invalidDatasetList after successful creation', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockInvalidDatasetList).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onHide after successful creation', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockOnHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show error notification on API failure', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.failed',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call onHide on API failure', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Wait for API call to complete
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalled()
|
||||
})
|
||||
// onHide should not be called on failure
|
||||
expect(mockOnHide).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not invalidate dataset list on API failure', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test Dataset' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockInvalidDatasetList).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Router Navigation - Test Next.js router
|
||||
// ==========================================
|
||||
describe('Router Navigation', () => {
|
||||
it('should navigate to dataset documents page after successful creation', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockResolvedValue({
|
||||
id: 'test-dataset-456',
|
||||
name: 'Test',
|
||||
} as ReturnType<typeof createEmptyDataset> extends Promise<infer T> ? T : never)
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-456/documents')
|
||||
})
|
||||
})
|
||||
|
||||
it('should not navigate on validation error', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Click confirm with empty input
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not navigate on API error', async () => {
|
||||
// Arrange
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases - Test boundary conditions and error handling
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle whitespace-only input as valid (component behavior)', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Enter whitespace only
|
||||
fireEvent.change(input, { target: { value: ' ' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Current implementation treats whitespace as valid input
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: ' ' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle special characters in input', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Test @#$% Dataset!' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Test @#$% Dataset!' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle Unicode characters in input', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: '数据集测试 🚀' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: '数据集测试 🚀' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle input at exactly 40 character boundary', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Test boundary: 40 characters is valid
|
||||
const name40Chars = 'A'.repeat(40)
|
||||
fireEvent.change(input, { target: { value: name40Chars } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: name40Chars })
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject input at 41 character boundary', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Test boundary: 41 characters is invalid
|
||||
const name41Chars = 'A'.repeat(41)
|
||||
fireEvent.change(input, { target: { value: name41Chars } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.nameLengthInvalid',
|
||||
})
|
||||
})
|
||||
expect(mockCreateEmptyDataset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle rapid consecutive submits', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Rapid clicks
|
||||
fireEvent.change(input, { target: { value: 'Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
fireEvent.click(confirmButton)
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - API will be called multiple times (no debounce in current implementation)
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle input with leading/trailing spaces', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: ' Dataset Name ' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Current implementation does not trim spaces
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: ' Dataset Name ' })
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle newline characters in input (browser strips newlines)', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Line1\nLine2' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - HTML input elements strip newline characters (expected browser behavior)
|
||||
await waitFor(() => {
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Line1Line2' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Validation Tests - Test input validation
|
||||
// ==========================================
|
||||
describe('Validation', () => {
|
||||
it('should not submit when input is empty string', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.nameNotEmpty',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate length before calling API', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'A'.repeat(50) } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Should show error before API call
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.nameLengthInvalid',
|
||||
})
|
||||
})
|
||||
expect(mockCreateEmptyDataset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should validate empty string before length check', async () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
render(<EmptyDatasetCreationModal {...props} />)
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act - Don't enter anything
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Should show empty error, not length error
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.nameNotEmpty',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Integration Tests - Test complete flows
|
||||
// ==========================================
|
||||
describe('Integration', () => {
|
||||
it('should complete full successful creation flow', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
mockCreateEmptyDataset.mockResolvedValue({
|
||||
id: 'new-id-789',
|
||||
name: 'Complete Flow Test',
|
||||
} as ReturnType<typeof createEmptyDataset> extends Promise<infer T> ? T : never)
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Complete Flow Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Verify complete flow
|
||||
await waitFor(() => {
|
||||
// 1. API called
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Complete Flow Test' })
|
||||
// 2. Dataset list invalidated
|
||||
expect(mockInvalidDatasetList).toHaveBeenCalled()
|
||||
// 3. Modal closed
|
||||
expect(mockOnHide).toHaveBeenCalled()
|
||||
// 4. Navigation happened
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/new-id-789/documents')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle error flow correctly', async () => {
|
||||
// Arrange
|
||||
const mockOnHide = jest.fn()
|
||||
mockCreateEmptyDataset.mockRejectedValue(new Error('Server Error'))
|
||||
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
|
||||
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
|
||||
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
|
||||
|
||||
// Act
|
||||
fireEvent.change(input, { target: { value: 'Error Test' } })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
// Assert - Verify error handling
|
||||
await waitFor(() => {
|
||||
// 1. API was called
|
||||
expect(mockCreateEmptyDataset).toHaveBeenCalled()
|
||||
// 2. Error notification shown
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'datasetCreation.stepOne.modal.failed',
|
||||
})
|
||||
})
|
||||
|
||||
// 3. These should NOT happen on error
|
||||
expect(mockInvalidDatasetList).not.toHaveBeenCalled()
|
||||
expect(mockOnHide).not.toHaveBeenCalled()
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user