test: add unit tests for UI components to improve coverage
Add comprehensive tests for: - Pagination: navigation, page numbers, disabled states - EmptyState: message, icon, title, action button - LoadingState: different types (page, card, table, list) - Modal: open/close, backdrop click, escape key, sizes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
79b579ae55
commit
43a2dadd0e
95
__tests__/components/ui/EmptyState.test.tsx
Normal file
95
__tests__/components/ui/EmptyState.test.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Tests for the EmptyState component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
|
||||
describe('EmptyState', () => {
|
||||
it('should render message', () => {
|
||||
render(<EmptyState message="No items found" />);
|
||||
expect(screen.getByText('No items found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render default icon', () => {
|
||||
render(<EmptyState message="No items" />);
|
||||
expect(screen.getByText('📝')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render custom icon', () => {
|
||||
render(<EmptyState message="No items" icon="🔍" />);
|
||||
expect(screen.getByText('🔍')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render title when provided', () => {
|
||||
render(<EmptyState message="No items found" title="Empty List" />);
|
||||
expect(screen.getByText('Empty List')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render title when not provided', () => {
|
||||
render(<EmptyState message="No items found" />);
|
||||
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render action button when action is provided', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(
|
||||
<EmptyState
|
||||
message="No items"
|
||||
action={{ label: 'Add Item', onClick: mockClick }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Add Item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call action onClick when button is clicked', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(
|
||||
<EmptyState
|
||||
message="No items"
|
||||
action={{ label: 'Add Item', onClick: mockClick }}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByText('Add Item'));
|
||||
expect(mockClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not render action button when action is not provided', () => {
|
||||
render(<EmptyState message="No items" />);
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have centered layout', () => {
|
||||
const { container } = render(<EmptyState message="No items" />);
|
||||
expect(container.firstChild).toHaveClass('text-center');
|
||||
expect(container.firstChild).toHaveClass('py-16');
|
||||
});
|
||||
|
||||
it('should style title correctly', () => {
|
||||
render(<EmptyState message="No items" title="Empty" />);
|
||||
const title = screen.getByText('Empty');
|
||||
expect(title).toHaveClass('text-lg');
|
||||
expect(title).toHaveClass('font-semibold');
|
||||
});
|
||||
|
||||
it('should style message correctly', () => {
|
||||
render(<EmptyState message="No items found" />);
|
||||
const message = screen.getByText('No items found');
|
||||
expect(message).toHaveClass('text-gray-600');
|
||||
});
|
||||
|
||||
it('should style action button correctly', () => {
|
||||
const mockClick = jest.fn();
|
||||
render(
|
||||
<EmptyState
|
||||
message="No items"
|
||||
action={{ label: 'Click Me', onClick: mockClick }}
|
||||
/>
|
||||
);
|
||||
const button = screen.getByText('Click Me');
|
||||
expect(button).toHaveClass('bg-blue-600');
|
||||
expect(button).toHaveClass('text-white');
|
||||
});
|
||||
});
|
||||
99
__tests__/components/ui/LoadingState.test.tsx
Normal file
99
__tests__/components/ui/LoadingState.test.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Tests for the LoadingState component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { LoadingState } from '@/components/ui/LoadingState';
|
||||
|
||||
describe('LoadingState', () => {
|
||||
it('should render page type by default', () => {
|
||||
const { container } = render(<LoadingState />);
|
||||
expect(container.querySelector('.p-8')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render card type skeleton', () => {
|
||||
const { container } = render(<LoadingState type="card" />);
|
||||
expect(container.querySelector('.h-32')).toBeInTheDocument();
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render table type skeleton', () => {
|
||||
const { container } = render(<LoadingState type="table" />);
|
||||
expect(container.querySelector('.h-10')).toBeInTheDocument();
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render table type with custom rows', () => {
|
||||
const { container } = render(<LoadingState type="table" rows={6} />);
|
||||
const rowElements = container.querySelectorAll('.h-12');
|
||||
expect(rowElements).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('should render list type skeleton', () => {
|
||||
const { container } = render(<LoadingState type="list" />);
|
||||
expect(container.querySelector('.rounded-full')).toBeInTheDocument();
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render list type with custom rows', () => {
|
||||
const { container } = render(<LoadingState type="list" rows={3} />);
|
||||
const avatars = container.querySelectorAll('.rounded-full');
|
||||
expect(avatars).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should render page type with default columns', () => {
|
||||
const { container } = render(<LoadingState type="page" />);
|
||||
const cards = container.querySelectorAll('.h-32');
|
||||
expect(cards).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should render page type with custom columns', () => {
|
||||
const { container } = render(<LoadingState type="page" columns={6} />);
|
||||
const cards = container.querySelectorAll('.h-32');
|
||||
expect(cards).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('should have animate-pulse class for card type', () => {
|
||||
const { container } = render(<LoadingState type="card" />);
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have animate-pulse class for table type', () => {
|
||||
const { container } = render(<LoadingState type="table" />);
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have animate-pulse class for list type', () => {
|
||||
const { container } = render(<LoadingState type="list" />);
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have animate-pulse class for page type', () => {
|
||||
const { container } = render(<LoadingState type="page" />);
|
||||
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with rounded corners for card type', () => {
|
||||
const { container } = render(<LoadingState type="card" />);
|
||||
expect(container.querySelector('.rounded-lg')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with gray backgrounds', () => {
|
||||
const { container } = render(<LoadingState type="card" />);
|
||||
expect(container.querySelector('.bg-gray-200')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should default to 4 rows for table type', () => {
|
||||
const { container } = render(<LoadingState type="table" />);
|
||||
const rowElements = container.querySelectorAll('.h-12');
|
||||
expect(rowElements).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should default to 4 rows for list type', () => {
|
||||
const { container } = render(<LoadingState type="list" />);
|
||||
const avatars = container.querySelectorAll('.rounded-full');
|
||||
expect(avatars).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
200
__tests__/components/ui/Modal.test.tsx
Normal file
200
__tests__/components/ui/Modal.test.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* Tests for the Modal component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
|
||||
describe('Modal', () => {
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not render when isOpen is false', () => {
|
||||
const { container } = render(
|
||||
<Modal isOpen={false} onClose={mockOnClose}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should render when isOpen is true', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Modal content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.getByText('Modal content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render title when provided', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} title="Test Title">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render close button by default', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} title="Test">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.getByLabelText('Fermer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render close button when showCloseButton is false', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} showCloseButton={false}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.queryByLabelText('Fermer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onClose when close button is clicked', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} title="Test">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText('Fermer'));
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onClose when backdrop is clicked', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
// The dialog role is on the backdrop element itself
|
||||
const backdrop = screen.getByRole('dialog');
|
||||
fireEvent.click(backdrop);
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call onClose when modal content is clicked', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
fireEvent.click(screen.getByText('Content'));
|
||||
expect(mockOnClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call onClose when Escape key is pressed', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
fireEvent.keyDown(document, { key: 'Escape' });
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should have dialog role', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have aria-modal attribute', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(screen.getByRole('dialog')).toHaveAttribute('aria-modal', 'true');
|
||||
});
|
||||
|
||||
it('should apply sm size class', () => {
|
||||
const { container } = render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} size="sm">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(container.querySelector('.max-w-md')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply md size class by default', () => {
|
||||
const { container } = render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(container.querySelector('.max-w-lg')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply lg size class', () => {
|
||||
const { container } = render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} size="lg">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(container.querySelector('.max-w-2xl')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply xl size class', () => {
|
||||
const { container } = render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} size="xl">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(container.querySelector('.max-w-4xl')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set body overflow to hidden when open', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(document.body.style.overflow).toBe('hidden');
|
||||
});
|
||||
|
||||
it('should reset body overflow when closed', () => {
|
||||
const { rerender } = render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
rerender(
|
||||
<Modal isOpen={false} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
expect(document.body.style.overflow).toBe('unset');
|
||||
});
|
||||
|
||||
it('should have aria-labelledby when title is provided', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose} title="My Modal">
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(dialog).toHaveAttribute('aria-labelledby', 'modal-title');
|
||||
});
|
||||
|
||||
it('should not have aria-labelledby when title is not provided', () => {
|
||||
render(
|
||||
<Modal isOpen={true} onClose={mockOnClose}>
|
||||
<div>Content</div>
|
||||
</Modal>
|
||||
);
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(dialog).not.toHaveAttribute('aria-labelledby');
|
||||
});
|
||||
});
|
||||
174
__tests__/components/ui/Pagination.test.tsx
Normal file
174
__tests__/components/ui/Pagination.test.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* Tests for the Pagination component
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
|
||||
describe('Pagination', () => {
|
||||
const mockOnPageChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return null when totalPages is 1', () => {
|
||||
const { container } = render(
|
||||
<Pagination currentPage={1} totalPages={1} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when totalPages is 0', () => {
|
||||
const { container } = render(
|
||||
<Pagination currentPage={1} totalPages={0} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should render previous and next buttons', () => {
|
||||
render(
|
||||
<Pagination currentPage={2} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
expect(screen.getByText('Précédent')).toBeInTheDocument();
|
||||
expect(screen.getByText('Suivant')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable previous button on first page', () => {
|
||||
render(
|
||||
<Pagination currentPage={1} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
const prevButton = screen.getByText('Précédent').closest('button');
|
||||
expect(prevButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable next button on last page', () => {
|
||||
render(
|
||||
<Pagination currentPage={5} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
const nextButton = screen.getByText('Suivant').closest('button');
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should call onPageChange with previous page when clicking previous', () => {
|
||||
render(
|
||||
<Pagination currentPage={3} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
fireEvent.click(screen.getByText('Précédent'));
|
||||
expect(mockOnPageChange).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should call onPageChange with next page when clicking next', () => {
|
||||
render(
|
||||
<Pagination currentPage={3} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
fireEvent.click(screen.getByText('Suivant'));
|
||||
expect(mockOnPageChange).toHaveBeenCalledWith(4);
|
||||
});
|
||||
|
||||
it('should not call onPageChange when clicking disabled previous', () => {
|
||||
render(
|
||||
<Pagination currentPage={1} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
fireEvent.click(screen.getByText('Précédent'));
|
||||
expect(mockOnPageChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call onPageChange when clicking disabled next', () => {
|
||||
render(
|
||||
<Pagination currentPage={5} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
fireEvent.click(screen.getByText('Suivant'));
|
||||
expect(mockOnPageChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show page numbers when showPageNumbers is true', () => {
|
||||
render(
|
||||
<Pagination
|
||||
currentPage={1}
|
||||
totalPages={3}
|
||||
onPageChange={mockOnPageChange}
|
||||
showPageNumbers={true}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show page text when showPageNumbers is false', () => {
|
||||
render(
|
||||
<Pagination
|
||||
currentPage={2}
|
||||
totalPages={5}
|
||||
onPageChange={mockOnPageChange}
|
||||
showPageNumbers={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Page 2 sur 5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onPageChange when clicking a page number', () => {
|
||||
render(
|
||||
<Pagination currentPage={1} totalPages={5} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
fireEvent.click(screen.getByText('3'));
|
||||
expect(mockOnPageChange).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it('should highlight current page', () => {
|
||||
render(
|
||||
<Pagination currentPage={2} totalPages={3} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
const currentPageButton = screen.getByText('2');
|
||||
expect(currentPageButton).toHaveClass('bg-blue-600');
|
||||
expect(currentPageButton).toHaveClass('text-white');
|
||||
});
|
||||
|
||||
it('should show ellipsis for many pages when at start', () => {
|
||||
render(
|
||||
<Pagination currentPage={2} totalPages={10} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
expect(screen.getByText('...')).toBeInTheDocument();
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show ellipsis for many pages when at end', () => {
|
||||
render(
|
||||
<Pagination currentPage={9} totalPages={10} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
expect(screen.getByText('...')).toBeInTheDocument();
|
||||
expect(screen.getByText('1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show two ellipsis when in middle of many pages', () => {
|
||||
render(
|
||||
<Pagination currentPage={5} totalPages={10} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
const ellipsis = screen.getAllByText('...');
|
||||
expect(ellipsis).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<Pagination
|
||||
currentPage={1}
|
||||
totalPages={3}
|
||||
onPageChange={mockOnPageChange}
|
||||
className="custom-class"
|
||||
/>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('should have flex layout', () => {
|
||||
const { container } = render(
|
||||
<Pagination currentPage={1} totalPages={3} onPageChange={mockOnPageChange} />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('flex');
|
||||
expect(container.firstChild).toHaveClass('items-center');
|
||||
expect(container.firstChild).toHaveClass('justify-center');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user