mirror of https://github.com/langgenius/dify.git
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:
parent
e228b802c5
commit
46e0548731
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,596 @@
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
import LanguageSelect from './index'
|
||||||
|
import type { ILanguageSelectProps } from './index'
|
||||||
|
import { languages } from '@/i18n-config/language'
|
||||||
|
|
||||||
|
// Get supported languages for test assertions
|
||||||
|
const supportedLanguages = languages.filter(lang => lang.supported)
|
||||||
|
|
||||||
|
// Test data builder for props
|
||||||
|
const createDefaultProps = (overrides?: Partial<ILanguageSelectProps>): ILanguageSelectProps => ({
|
||||||
|
currentLanguage: 'English',
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
disabled: false,
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('LanguageSelect', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Rendering Tests - Verify component renders correctly
|
||||||
|
// ==========================================
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('English')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render current language text', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'Chinese Simplified' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Chinese Simplified')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render dropdown arrow icon', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - RiArrowDownSLine renders as SVG
|
||||||
|
const svgIcon = container.querySelector('svg')
|
||||||
|
expect(svgIcon).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render all supported languages in dropdown when opened', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act - Click button to open dropdown
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - All supported languages should be visible
|
||||||
|
// Use getAllByText because current language appears both in button and dropdown
|
||||||
|
supportedLanguages.forEach((lang) => {
|
||||||
|
expect(screen.getAllByText(lang.prompt_name).length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render check icon for selected language', () => {
|
||||||
|
// Arrange
|
||||||
|
const selectedLanguage = 'Japanese'
|
||||||
|
const props = createDefaultProps({ currentLanguage: selectedLanguage })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - The selected language option should have a check icon
|
||||||
|
const languageOptions = screen.getAllByText(selectedLanguage)
|
||||||
|
// One in the button, one in the dropdown list
|
||||||
|
expect(languageOptions.length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Props Testing - Verify all prop variations work correctly
|
||||||
|
// ==========================================
|
||||||
|
describe('Props', () => {
|
||||||
|
describe('currentLanguage prop', () => {
|
||||||
|
it('should display English when currentLanguage is English', () => {
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'English' })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
expect(screen.getByText('English')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display Chinese Simplified when currentLanguage is Chinese Simplified', () => {
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'Chinese Simplified' })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
expect(screen.getByText('Chinese Simplified')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display Japanese when currentLanguage is Japanese', () => {
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'Japanese' })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
expect(screen.getByText('Japanese')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each(supportedLanguages.map(l => l.prompt_name))(
|
||||||
|
'should display %s as current language',
|
||||||
|
(language) => {
|
||||||
|
const props = createDefaultProps({ currentLanguage: language })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
expect(screen.getByText(language)).toBeInTheDocument()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('disabled prop', () => {
|
||||||
|
it('should have disabled button when disabled is true', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: true })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have enabled button when disabled is false', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: false })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have enabled button when disabled is undefined', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
delete (props as Partial<ILanguageSelectProps>).disabled
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).not.toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply disabled styling when disabled is true', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: true })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check for disabled class on text elements
|
||||||
|
const disabledTextElement = container.querySelector('.text-components-button-tertiary-text-disabled')
|
||||||
|
expect(disabledTextElement).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply cursor-not-allowed styling when disabled', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: true })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const elementWithCursor = container.querySelector('.cursor-not-allowed')
|
||||||
|
expect(elementWithCursor).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('onSelect prop', () => {
|
||||||
|
it('should be callable as a function', () => {
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Open dropdown and click a language
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
const germanOption = screen.getByText('German')
|
||||||
|
fireEvent.click(germanOption)
|
||||||
|
|
||||||
|
expect(mockOnSelect).toHaveBeenCalledWith('German')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// User Interactions - Test event handlers
|
||||||
|
// ==========================================
|
||||||
|
describe('User Interactions', () => {
|
||||||
|
it('should open dropdown when button is clicked', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Check if dropdown content is visible
|
||||||
|
expect(screen.getAllByText('English').length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onSelect when a language option is clicked', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
const frenchOption = screen.getByText('French')
|
||||||
|
fireEvent.click(frenchOption)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockOnSelect).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockOnSelect).toHaveBeenCalledWith('French')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call onSelect with correct language when selecting different languages', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act & Assert - Test multiple language selections
|
||||||
|
const testLanguages = ['Korean', 'Spanish', 'Italian']
|
||||||
|
|
||||||
|
testLanguages.forEach((lang) => {
|
||||||
|
mockOnSelect.mockClear()
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
const languageOption = screen.getByText(lang)
|
||||||
|
fireEvent.click(languageOption)
|
||||||
|
expect(mockOnSelect).toHaveBeenCalledWith(lang)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not open dropdown when disabled', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: true })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Dropdown should not open, only one instance of the current language should exist
|
||||||
|
const englishElements = screen.getAllByText('English')
|
||||||
|
expect(englishElements.length).toBe(1) // Only the button text, not dropdown
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not call onSelect when component is disabled', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect, disabled: true })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockOnSelect).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle rapid consecutive clicks', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act - Rapid clicks
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
fireEvent.click(button)
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Component should not crash
|
||||||
|
expect(button).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Component Memoization - Test React.memo behavior
|
||||||
|
// ==========================================
|
||||||
|
describe('Memoization', () => {
|
||||||
|
it('should be wrapped with React.memo', () => {
|
||||||
|
// Assert - Check component has memo wrapper
|
||||||
|
expect(LanguageSelect.$$typeof).toBe(Symbol.for('react.memo'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not re-render when props remain the same', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||||
|
const renderSpy = jest.fn()
|
||||||
|
|
||||||
|
// Create a wrapper component to track renders
|
||||||
|
const TrackedLanguageSelect: React.FC<ILanguageSelectProps> = (trackedProps) => {
|
||||||
|
renderSpy()
|
||||||
|
return <LanguageSelect {...trackedProps} />
|
||||||
|
}
|
||||||
|
const MemoizedTracked = React.memo(TrackedLanguageSelect)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<MemoizedTracked {...props} />)
|
||||||
|
rerender(<MemoizedTracked {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should only render once due to same props
|
||||||
|
expect(renderSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-render when currentLanguage changes', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'English' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<LanguageSelect {...props} />)
|
||||||
|
expect(screen.getByText('English')).toBeInTheDocument()
|
||||||
|
|
||||||
|
rerender(<LanguageSelect {...props} currentLanguage="French" />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('French')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-render when disabled changes', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: false })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<LanguageSelect {...props} />)
|
||||||
|
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||||
|
|
||||||
|
rerender(<LanguageSelect {...props} disabled={true} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByRole('button')).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Edge Cases - Test boundary conditions and error handling
|
||||||
|
// ==========================================
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle empty string as currentLanguage', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ currentLanguage: '' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - Component should still render
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle non-existent language as currentLanguage', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'NonExistentLanguage' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should display the value even if not in list
|
||||||
|
expect(screen.getByText('NonExistentLanguage')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle special characters in language names', () => {
|
||||||
|
// Arrange - Turkish has special character in prompt_name
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'Türkçe' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Türkçe')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle very long language names', () => {
|
||||||
|
// Arrange
|
||||||
|
const longLanguageName = 'A'.repeat(100)
|
||||||
|
const props = createDefaultProps({ currentLanguage: longLanguageName })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should not crash and should display the text
|
||||||
|
expect(screen.getByText(longLanguageName)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render correct number of language options', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Should show all supported languages
|
||||||
|
const expectedCount = supportedLanguages.length
|
||||||
|
// Each language appears in the dropdown (use getAllByText because current language appears twice)
|
||||||
|
supportedLanguages.forEach((lang) => {
|
||||||
|
expect(screen.getAllByText(lang.prompt_name).length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
expect(supportedLanguages.length).toBe(expectedCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should only show supported languages in dropdown', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - All displayed languages should be supported
|
||||||
|
const allLanguages = languages
|
||||||
|
const unsupportedLanguages = allLanguages.filter(lang => !lang.supported)
|
||||||
|
|
||||||
|
unsupportedLanguages.forEach((lang) => {
|
||||||
|
expect(screen.queryByText(lang.prompt_name)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle undefined onSelect gracefully when clicking', () => {
|
||||||
|
// Arrange - This tests TypeScript boundary, but runtime should not crash
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
const option = screen.getByText('German')
|
||||||
|
|
||||||
|
// Assert - Should not throw
|
||||||
|
expect(() => fireEvent.click(option)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should maintain selection state visually with check icon', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ currentLanguage: 'Russian' })
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Find the check icon (RiCheckLine) in the dropdown
|
||||||
|
// The selected option should have a check icon next to it
|
||||||
|
const checkIcons = container.querySelectorAll('svg.text-text-accent')
|
||||||
|
expect(checkIcons.length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Accessibility - Basic accessibility checks
|
||||||
|
// ==========================================
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('should have accessible button element', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
expect(button).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have clickable language options', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Options should be clickable (have cursor-pointer class)
|
||||||
|
const options = screen.getAllByText(/English|French|German|Japanese/i)
|
||||||
|
expect(options.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Integration with Popover - Test Popover behavior
|
||||||
|
// ==========================================
|
||||||
|
describe('Popover Integration', () => {
|
||||||
|
it('should use manualClose prop on Popover', () => {
|
||||||
|
// Arrange
|
||||||
|
const mockOnSelect = jest.fn()
|
||||||
|
const props = createDefaultProps({ onSelect: mockOnSelect })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<LanguageSelect {...props} />)
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Popover should be open
|
||||||
|
expect(screen.getAllByText('English').length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have correct popup z-index class', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Check for z-20 class (popupClassName='z-20')
|
||||||
|
// This is applied to the Popover
|
||||||
|
expect(container.querySelector('.z-20')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Styling Tests - Verify correct CSS classes applied
|
||||||
|
// ==========================================
|
||||||
|
describe('Styling', () => {
|
||||||
|
it('should apply tertiary button styling', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check for tertiary button classes (uses ! prefix for important)
|
||||||
|
expect(container.querySelector('.\\!bg-components-button-tertiary-bg')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply hover styling class to options', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Options should have hover class
|
||||||
|
const optionWithHover = container.querySelector('.hover\\:bg-state-base-hover')
|
||||||
|
expect(optionWithHover).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply correct text styling to language options', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const button = screen.getByRole('button')
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Assert - Check for system-sm-medium class on options
|
||||||
|
const styledOption = container.querySelector('.system-sm-medium')
|
||||||
|
expect(styledOption).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply disabled styling to icon when disabled', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ disabled: true })
|
||||||
|
const { container } = render(<LanguageSelect {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check for disabled text color on icon
|
||||||
|
const disabledIcon = container.querySelector('.text-components-button-tertiary-text-disabled')
|
||||||
|
expect(disabledIcon).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,803 @@
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
import PreviewItem, { PreviewType } from './index'
|
||||||
|
import type { IPreviewItemProps } from './index'
|
||||||
|
|
||||||
|
// Test data builder for props
|
||||||
|
const createDefaultProps = (overrides?: Partial<IPreviewItemProps>): IPreviewItemProps => ({
|
||||||
|
type: PreviewType.TEXT,
|
||||||
|
index: 1,
|
||||||
|
content: 'Test content',
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createQAProps = (overrides?: Partial<IPreviewItemProps>): IPreviewItemProps => ({
|
||||||
|
type: PreviewType.QA,
|
||||||
|
index: 1,
|
||||||
|
qa: {
|
||||||
|
question: 'Test question',
|
||||||
|
answer: 'Test answer',
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('PreviewItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Rendering Tests - Verify component renders correctly
|
||||||
|
// ==========================================
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render without crashing', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Test content')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render with TEXT type', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: 'Sample text content' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Sample text content')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render with QA type', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Test question')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Test answer')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render sharp icon (#) with formatted index', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index: 5 })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Index should be padded to 3 digits
|
||||||
|
expect(screen.getByText('005')).toBeInTheDocument()
|
||||||
|
// Sharp icon SVG should exist
|
||||||
|
const svgElements = container.querySelectorAll('svg')
|
||||||
|
expect(svgElements.length).toBeGreaterThanOrEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render character count for TEXT type', () => {
|
||||||
|
// Arrange
|
||||||
|
const content = 'Hello World' // 11 characters
|
||||||
|
const props = createDefaultProps({ content })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Shows character count with translation key
|
||||||
|
expect(screen.getByText(/11/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/datasetCreation.stepTwo.characters/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render character count for QA type', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: {
|
||||||
|
question: 'Hello', // 5 characters
|
||||||
|
answer: 'World', // 5 characters - total 10
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Shows combined character count
|
||||||
|
expect(screen.getByText(/10/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render text icon SVG', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should have SVG icons
|
||||||
|
const svgElements = container.querySelectorAll('svg')
|
||||||
|
expect(svgElements.length).toBe(2) // Sharp icon and text icon
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Props Testing - Verify all prop variations work correctly
|
||||||
|
// ==========================================
|
||||||
|
describe('Props', () => {
|
||||||
|
describe('type prop', () => {
|
||||||
|
it('should render TEXT content when type is TEXT', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ type: PreviewType.TEXT, content: 'Text mode content' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Text mode content')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Q')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('A')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render QA content when type is QA', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
type: PreviewType.QA,
|
||||||
|
qa: { question: 'My question', answer: 'My answer' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('My question')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('My answer')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use TEXT as default type when type is "text"', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ type: 'text' as PreviewType, content: 'Default type content' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Default type content')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use QA type when type is "QA"', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({ type: 'QA' as PreviewType })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('index prop', () => {
|
||||||
|
it.each([
|
||||||
|
[1, '001'],
|
||||||
|
[5, '005'],
|
||||||
|
[10, '010'],
|
||||||
|
[99, '099'],
|
||||||
|
[100, '100'],
|
||||||
|
[999, '999'],
|
||||||
|
[1000, '1000'],
|
||||||
|
])('should format index %i as %s', (index, expected) => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(expected)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle index 0', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index: 0 })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('000')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle large index numbers', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index: 12345 })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('12345')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('content prop', () => {
|
||||||
|
it('should render content when provided', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: 'Custom content here' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Custom content here')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle multiline content', () => {
|
||||||
|
// Arrange
|
||||||
|
const multilineContent = 'Line 1\nLine 2\nLine 3'
|
||||||
|
const props = createDefaultProps({ content: multilineContent })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check content is rendered (multiline text is in pre-line div)
|
||||||
|
const contentDiv = container.querySelector('[style*="white-space: pre-line"]')
|
||||||
|
expect(contentDiv?.textContent).toContain('Line 1')
|
||||||
|
expect(contentDiv?.textContent).toContain('Line 2')
|
||||||
|
expect(contentDiv?.textContent).toContain('Line 3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve whitespace with pre-line style', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: 'Text with spaces' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check for whiteSpace: pre-line style
|
||||||
|
const contentDiv = container.querySelector('[style*="white-space: pre-line"]')
|
||||||
|
expect(contentDiv).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('qa prop', () => {
|
||||||
|
it('should render question and answer when qa is provided', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: {
|
||||||
|
question: 'What is testing?',
|
||||||
|
answer: 'Testing is verification.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('What is testing?')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Testing is verification.')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render Q and A labels', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle multiline question', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: {
|
||||||
|
question: 'Question line 1\nQuestion line 2',
|
||||||
|
answer: 'Answer',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check content is in pre-line div
|
||||||
|
const preLineDivs = container.querySelectorAll('[style*="white-space: pre-line"]')
|
||||||
|
const questionDiv = Array.from(preLineDivs).find(div => div.textContent?.includes('Question line 1'))
|
||||||
|
expect(questionDiv).toBeTruthy()
|
||||||
|
expect(questionDiv?.textContent).toContain('Question line 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle multiline answer', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: {
|
||||||
|
question: 'Question',
|
||||||
|
answer: 'Answer line 1\nAnswer line 2',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check content is in pre-line div
|
||||||
|
const preLineDivs = container.querySelectorAll('[style*="white-space: pre-line"]')
|
||||||
|
const answerDiv = Array.from(preLineDivs).find(div => div.textContent?.includes('Answer line 1'))
|
||||||
|
expect(answerDiv).toBeTruthy()
|
||||||
|
expect(answerDiv?.textContent).toContain('Answer line 2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Component Memoization - Test React.memo behavior
|
||||||
|
// ==========================================
|
||||||
|
describe('Memoization', () => {
|
||||||
|
it('should be wrapped with React.memo', () => {
|
||||||
|
// Assert - Check component has memo wrapper
|
||||||
|
expect(PreviewItem.$$typeof).toBe(Symbol.for('react.memo'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not re-render when props remain the same', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
const renderSpy = jest.fn()
|
||||||
|
|
||||||
|
// Create a wrapper component to track renders
|
||||||
|
const TrackedPreviewItem: React.FC<IPreviewItemProps> = (trackedProps) => {
|
||||||
|
renderSpy()
|
||||||
|
return <PreviewItem {...trackedProps} />
|
||||||
|
}
|
||||||
|
const MemoizedTracked = React.memo(TrackedPreviewItem)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<MemoizedTracked {...props} />)
|
||||||
|
rerender(<MemoizedTracked {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should only render once due to same props
|
||||||
|
expect(renderSpy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-render when content changes', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: 'Initial content' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<PreviewItem {...props} />)
|
||||||
|
expect(screen.getByText('Initial content')).toBeInTheDocument()
|
||||||
|
|
||||||
|
rerender(<PreviewItem {...props} content="Updated content" />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Updated content')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-render when index changes', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index: 1 })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<PreviewItem {...props} />)
|
||||||
|
expect(screen.getByText('001')).toBeInTheDocument()
|
||||||
|
|
||||||
|
rerender(<PreviewItem {...props} index={99} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('099')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-render when type changes', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ type: PreviewType.TEXT, content: 'Text content' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<PreviewItem {...props} />)
|
||||||
|
expect(screen.getByText('Text content')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Q')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
rerender(<PreviewItem type={PreviewType.QA} index={1} qa={{ question: 'Q1', answer: 'A1' }} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should re-render when qa prop changes', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: { question: 'Original question', answer: 'Original answer' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { rerender } = render(<PreviewItem {...props} />)
|
||||||
|
expect(screen.getByText('Original question')).toBeInTheDocument()
|
||||||
|
|
||||||
|
rerender(<PreviewItem {...props} qa={{ question: 'New question', answer: 'New answer' }} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('New question')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('New answer')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Edge Cases - Test boundary conditions and error handling
|
||||||
|
// ==========================================
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
describe('Empty/Undefined values', () => {
|
||||||
|
it('should handle undefined content gracefully', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: undefined })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should show 0 characters (use more specific text match)
|
||||||
|
expect(screen.getByText(/^0 datasetCreation/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty string content', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: '' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should show 0 characters (use more specific text match)
|
||||||
|
expect(screen.getByText(/^0 datasetCreation/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle undefined qa gracefully', () => {
|
||||||
|
// Arrange
|
||||||
|
const props: IPreviewItemProps = {
|
||||||
|
type: PreviewType.QA,
|
||||||
|
index: 1,
|
||||||
|
qa: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should render Q and A labels but with empty content
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
// Character count should be 0 (use more specific text match)
|
||||||
|
expect(screen.getByText(/^0 datasetCreation/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle undefined question in qa', () => {
|
||||||
|
// Arrange
|
||||||
|
const props: IPreviewItemProps = {
|
||||||
|
type: PreviewType.QA,
|
||||||
|
index: 1,
|
||||||
|
qa: {
|
||||||
|
question: undefined as unknown as string,
|
||||||
|
answer: 'Only answer',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Only answer')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle undefined answer in qa', () => {
|
||||||
|
// Arrange
|
||||||
|
const props: IPreviewItemProps = {
|
||||||
|
type: PreviewType.QA,
|
||||||
|
index: 1,
|
||||||
|
qa: {
|
||||||
|
question: 'Only question',
|
||||||
|
answer: undefined as unknown as string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Only question')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty question and answer strings', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: { question: '', answer: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should show 0 characters (use more specific text match)
|
||||||
|
expect(screen.getByText(/^0 datasetCreation/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Character count calculation', () => {
|
||||||
|
it('should calculate correct character count for TEXT type', () => {
|
||||||
|
// Arrange - 'Test' has 4 characters
|
||||||
|
const props = createDefaultProps({ content: 'Test' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(/4/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should calculate correct character count for QA type (question + answer)', () => {
|
||||||
|
// Arrange - 'ABC' (3) + 'DEFGH' (5) = 8 characters
|
||||||
|
const props = createQAProps({
|
||||||
|
qa: { question: 'ABC', answer: 'DEFGH' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(/8/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should count special characters correctly', () => {
|
||||||
|
// Arrange - Content with special characters
|
||||||
|
const props = createDefaultProps({ content: '你好世界' }) // 4 Chinese characters
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(/4/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should count newlines in character count', () => {
|
||||||
|
// Arrange - 'a\nb' has 3 characters
|
||||||
|
const props = createDefaultProps({ content: 'a\nb' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(/3/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should count spaces in character count', () => {
|
||||||
|
// Arrange - 'a b' has 3 characters
|
||||||
|
const props = createDefaultProps({ content: 'a b' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(/3/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Boundary conditions', () => {
|
||||||
|
it('should handle very long content', () => {
|
||||||
|
// Arrange
|
||||||
|
const longContent = 'A'.repeat(10000)
|
||||||
|
const props = createDefaultProps({ content: longContent })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should show correct character count
|
||||||
|
expect(screen.getByText(/10000/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle very long index', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index: 999999999 })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('999999999')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle negative index', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ index: -1 })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - padStart pads from the start, so -1 becomes 0-1
|
||||||
|
expect(screen.getByText('0-1')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle content with only whitespace', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: ' ' }) // 3 spaces
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText(/3/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle content with HTML-like characters', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: '<div>Test</div>' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should render as text, not HTML
|
||||||
|
expect(screen.getByText('<div>Test</div>')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle content with emojis', () => {
|
||||||
|
// Arrange - Emojis can have complex character lengths
|
||||||
|
const props = createDefaultProps({ content: '😀👍' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Emoji length depends on JS string length
|
||||||
|
expect(screen.getByText('😀👍')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Type edge cases', () => {
|
||||||
|
it('should ignore qa prop when type is TEXT', () => {
|
||||||
|
// Arrange - Both content and qa provided, but type is TEXT
|
||||||
|
const props: IPreviewItemProps = {
|
||||||
|
type: PreviewType.TEXT,
|
||||||
|
index: 1,
|
||||||
|
content: 'Text content',
|
||||||
|
qa: { question: 'Should not show', answer: 'Also should not show' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.getByText('Text content')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Should not show')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Also should not show')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use content length for TEXT type even when qa is provided', () => {
|
||||||
|
// Arrange
|
||||||
|
const props: IPreviewItemProps = {
|
||||||
|
type: PreviewType.TEXT,
|
||||||
|
index: 1,
|
||||||
|
content: 'Hi', // 2 characters
|
||||||
|
qa: { question: 'Question', answer: 'Answer' }, // Would be 14 characters if used
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Should show 2, not 14
|
||||||
|
expect(screen.getByText(/2/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should ignore content prop when type is QA', () => {
|
||||||
|
// Arrange
|
||||||
|
const props: IPreviewItemProps = {
|
||||||
|
type: PreviewType.QA,
|
||||||
|
index: 1,
|
||||||
|
content: 'Should not display',
|
||||||
|
qa: { question: 'Q text', answer: 'A text' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(screen.queryByText('Should not display')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Q text')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('A text')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// PreviewType Enum - Test exported enum values
|
||||||
|
// ==========================================
|
||||||
|
describe('PreviewType Enum', () => {
|
||||||
|
it('should have TEXT value as "text"', () => {
|
||||||
|
expect(PreviewType.TEXT).toBe('text')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have QA value as "QA"', () => {
|
||||||
|
expect(PreviewType.QA).toBe('QA')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Styling Tests - Verify correct CSS classes applied
|
||||||
|
// ==========================================
|
||||||
|
describe('Styling', () => {
|
||||||
|
it('should have rounded container with gray background', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const rootDiv = container.firstChild as HTMLElement
|
||||||
|
expect(rootDiv).toHaveClass('rounded-xl', 'bg-gray-50', 'p-4')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have proper header styling', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - Check header div styling
|
||||||
|
const headerDiv = container.querySelector('.flex.h-5.items-center.justify-between')
|
||||||
|
expect(headerDiv).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have index badge styling', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const indexBadge = container.querySelector('.border.border-gray-200')
|
||||||
|
expect(indexBadge).toBeInTheDocument()
|
||||||
|
expect(indexBadge).toHaveClass('rounded-md', 'italic', 'font-medium')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have content area with line-clamp', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const contentArea = container.querySelector('.line-clamp-6')
|
||||||
|
expect(contentArea).toBeInTheDocument()
|
||||||
|
expect(contentArea).toHaveClass('max-h-[120px]', 'overflow-hidden')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have Q/A labels with gray color', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createQAProps()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { container } = render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const labels = container.querySelectorAll('.text-gray-400')
|
||||||
|
expect(labels.length).toBeGreaterThanOrEqual(2) // Q and A labels
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// i18n Translation - Test translation integration
|
||||||
|
// ==========================================
|
||||||
|
describe('i18n Translation', () => {
|
||||||
|
it('should use translation key for characters label', () => {
|
||||||
|
// Arrange
|
||||||
|
const props = createDefaultProps({ content: 'Test' })
|
||||||
|
|
||||||
|
// Act
|
||||||
|
render(<PreviewItem {...props} />)
|
||||||
|
|
||||||
|
// Assert - The mock returns the key as-is
|
||||||
|
expect(screen.getByText(/datasetCreation.stepTwo.characters/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -1,5 +1,20 @@
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import { cleanup } from '@testing-library/react'
|
import { cleanup } from '@testing-library/react'
|
||||||
|
import { mockAnimationsApi } from 'jsdom-testing-mocks'
|
||||||
|
|
||||||
|
// Mock Web Animations API for Headless UI
|
||||||
|
mockAnimationsApi()
|
||||||
|
|
||||||
|
// Suppress act() warnings from @headlessui/react internal Transition component
|
||||||
|
// These warnings are caused by Headless UI's internal async state updates, not our code
|
||||||
|
const originalConsoleError = console.error
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
// Check all arguments for the Headless UI TransitionRootFn act warning
|
||||||
|
const fullMessage = args.map(arg => (typeof arg === 'string' ? arg : '')).join(' ')
|
||||||
|
if (fullMessage.includes('TransitionRootFn') && fullMessage.includes('not wrapped in act'))
|
||||||
|
return
|
||||||
|
originalConsoleError.apply(console, args)
|
||||||
|
}
|
||||||
|
|
||||||
// Fix for @headlessui/react compatibility with happy-dom
|
// Fix for @headlessui/react compatibility with happy-dom
|
||||||
// headlessui tries to override focus properties which may be read-only in happy-dom
|
// headlessui tries to override focus properties which may be read-only in happy-dom
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
"jsdom-testing-mocks": "^1.16.0",
|
||||||
"knip": "^5.66.1",
|
"knip": "^5.66.1",
|
||||||
"lint-staged": "^15.5.2",
|
"lint-staged": "^15.5.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
||||||
|
|
@ -515,6 +515,9 @@ importers:
|
||||||
jest:
|
jest:
|
||||||
specifier: ^29.7.0
|
specifier: ^29.7.0
|
||||||
version: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.9.3))
|
version: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.9.3))
|
||||||
|
jsdom-testing-mocks:
|
||||||
|
specifier: ^1.16.0
|
||||||
|
version: 1.16.0
|
||||||
knip:
|
knip:
|
||||||
specifier: ^5.66.1
|
specifier: ^5.66.1
|
||||||
version: 5.72.0(@types/node@18.15.0)(typescript@5.9.3)
|
version: 5.72.0(@types/node@18.15.0)(typescript@5.9.3)
|
||||||
|
|
@ -4190,6 +4193,9 @@ packages:
|
||||||
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
|
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
bezier-easing@2.1.0:
|
||||||
|
resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==}
|
||||||
|
|
||||||
big.js@5.2.2:
|
big.js@5.2.2:
|
||||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||||
|
|
||||||
|
|
@ -4660,6 +4666,9 @@ packages:
|
||||||
webpack:
|
webpack:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
css-mediaquery@0.1.2:
|
||||||
|
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
|
||||||
|
|
||||||
css-select@4.3.0:
|
css-select@4.3.0:
|
||||||
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
||||||
|
|
||||||
|
|
@ -6317,6 +6326,10 @@ packages:
|
||||||
resolution: {integrity: sha512-F9GQ+F1ZU6qvSrZV8fNFpjDNf614YzR2eF6S0+XbDjAcUI28FSoXnYZFjQmb1kFx3rrJb5PnxUH3/Yti6fcM+g==}
|
resolution: {integrity: sha512-F9GQ+F1ZU6qvSrZV8fNFpjDNf614YzR2eF6S0+XbDjAcUI28FSoXnYZFjQmb1kFx3rrJb5PnxUH3/Yti6fcM+g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
jsdom-testing-mocks@1.16.0:
|
||||||
|
resolution: {integrity: sha512-wLrulXiLpjmcUYOYGEvz4XARkrmdVpyxzdBl9IAMbQ+ib2/UhUTRCn49McdNfXLff2ysGBUms49ZKX0LR1Q0gg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
jsesc@3.0.2:
|
jsesc@3.0.2:
|
||||||
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
|
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -13070,6 +13083,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
open: 8.4.2
|
open: 8.4.2
|
||||||
|
|
||||||
|
bezier-easing@2.1.0: {}
|
||||||
|
|
||||||
big.js@5.2.2: {}
|
big.js@5.2.2: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
@ -13577,6 +13592,8 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)
|
webpack: 5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)
|
||||||
|
|
||||||
|
css-mediaquery@0.1.2: {}
|
||||||
|
|
||||||
css-select@4.3.0:
|
css-select@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
|
|
@ -15682,6 +15699,11 @@ snapshots:
|
||||||
|
|
||||||
jsdoc-type-pratt-parser@5.4.0: {}
|
jsdoc-type-pratt-parser@5.4.0: {}
|
||||||
|
|
||||||
|
jsdom-testing-mocks@1.16.0:
|
||||||
|
dependencies:
|
||||||
|
bezier-easing: 2.1.0
|
||||||
|
css-mediaquery: 0.1.2
|
||||||
|
|
||||||
jsesc@3.0.2: {}
|
jsesc@3.0.2: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue