import type { MockedFunction } from 'vitest' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { createEmptyDataset } from '@/service/datasets' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' import EmptyDatasetCreationModal from './index' // Mock Next.js router const mockPush = vi.fn() vi.mock('next/navigation', () => ({ useRouter: () => ({ push: mockPush, }), })) // Mock createEmptyDataset API vi.mock('@/service/datasets', () => ({ createEmptyDataset: vi.fn(), })) // Mock useInvalidDatasetList hook vi.mock('@/service/knowledge/use-dataset', () => ({ useInvalidDatasetList: vi.fn(), })) // Mock ToastContext - need to mock both createContext and useContext from use-context-selector const mockNotify = vi.fn() vi.mock('use-context-selector', () => ({ createContext: vi.fn(() => ({ Provider: ({ children }: { children: React.ReactNode }) => children, })), useContext: vi.fn(() => ({ notify: mockNotify })), })) // Type cast mocked functions const mockCreateEmptyDataset = createEmptyDataset as MockedFunction const mockInvalidDatasetList = vi.fn() const mockUseInvalidDatasetList = useInvalidDatasetList as MockedFunction // Test data builder for props const createDefaultProps = (overrides?: Partial<{ show: boolean, onHide: () => void }>) => ({ show: true, onHide: vi.fn(), ...overrides, }) describe('EmptyDatasetCreationModal', () => { beforeEach(() => { vi.clearAllMocks() mockUseInvalidDatasetList.mockReturnValue(mockInvalidDatasetList) mockCreateEmptyDataset.mockResolvedValue({ id: 'dataset-123', name: 'Test Dataset', } as ReturnType extends Promise ? 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() // 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() // 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() // 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() // 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() // Assert expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument() }) it('should hide modal when show is false', () => { // Arrange & Act render() // Assert expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument() }) it('should toggle visibility when show prop changes', () => { // Arrange const onHide = vi.fn() const { rerender } = render() // Act & Assert - Initially hidden expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument() // Act & Assert - Show modal rerender() expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument() }) }) describe('onHide prop', () => { it('should call onHide when cancel button is clicked', () => { // Arrange const mockOnHide = vi.fn() render() // 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 = vi.fn() render() // 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() 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 = vi.fn() const { rerender } = render() 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() rerender() // 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() 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 = vi.fn() render() 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() 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() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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() 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 extends Promise ? T : never) const mockOnHide = vi.fn() render() 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() 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() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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() 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 = vi.fn() render() 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 = vi.fn() render() 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 = vi.fn() render() 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() 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() 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() 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 = vi.fn() mockCreateEmptyDataset.mockResolvedValue({ id: 'new-id-789', name: 'Complete Flow Test', } as ReturnType extends Promise ? T : never) render() 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 = vi.fn() mockCreateEmptyDataset.mockRejectedValue(new Error('Server Error')) render() 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() }) }) })