Implemented comprehensive loading and error handling components for the WellNuo web application with full test coverage. Components added: - LoadingSpinner: Configurable spinner with sizes, colors, and variants * LoadingOverlay: Full-screen loading with backdrop options * InlineLoader: Small inline spinner for buttons * PageLoader: Full-page centered loading state - ErrorMessage: Inline error messages with severity levels * FullScreenError: Full-page error states with retry/back actions * FieldError: Form field validation errors * ErrorBoundaryFallback: Error boundary fallback component * EmptyState: Empty data state component - Skeleton: Animated loading skeletons * SkeletonAvatar: Circular avatar skeleton * SkeletonText: Multi-line text skeleton * SkeletonCard: Card-style skeleton * SkeletonList: List of skeleton cards * SkeletonTable: Table skeleton with rows/columns * SkeletonDashboard: Dashboard-style skeleton layout * SkeletonForm: Form skeleton with fields Technical details: - Tailwind CSS styling with cn() utility function - Full accessibility support (ARIA labels, roles) - Comprehensive test coverage (57 tests, all passing) - TypeScript strict mode compliance - Added clsx and tailwind-merge dependencies Files: - web/components/ui/LoadingSpinner.tsx - web/components/ui/ErrorMessage.tsx - web/components/ui/Skeleton.tsx - web/components/ui/index.ts - web/lib/utils.ts - web/components/ui/__tests__/*.test.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import {
|
|
LoadingSpinner,
|
|
LoadingOverlay,
|
|
InlineLoader,
|
|
PageLoader,
|
|
} from '../LoadingSpinner';
|
|
|
|
describe('LoadingSpinner Component', () => {
|
|
it('renders basic spinner', () => {
|
|
render(<LoadingSpinner />);
|
|
const spinner = screen.getByRole('status');
|
|
expect(spinner).toBeInTheDocument();
|
|
expect(spinner).toHaveAttribute('aria-label', 'Loading');
|
|
});
|
|
|
|
it('renders with custom message', () => {
|
|
render(<LoadingSpinner message="Loading data..." />);
|
|
expect(screen.getByText('Loading data...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders different sizes', () => {
|
|
const { rerender } = render(<LoadingSpinner size="sm" />);
|
|
let spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('h-4', 'w-4');
|
|
|
|
rerender(<LoadingSpinner size="lg" />);
|
|
spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('h-12', 'w-12');
|
|
});
|
|
|
|
it('renders in full screen mode', () => {
|
|
render(<LoadingSpinner fullScreen message="Please wait..." />);
|
|
const container = screen.getByRole('status').parentElement;
|
|
expect(container).toHaveClass('fixed', 'inset-0', 'z-50');
|
|
expect(screen.getByText('Please wait...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
render(<LoadingSpinner className="custom-class" />);
|
|
const spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('renders with different colors', () => {
|
|
const { rerender } = render(<LoadingSpinner color="primary" />);
|
|
let spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('border-blue-600');
|
|
|
|
rerender(<LoadingSpinner color="white" />);
|
|
spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('border-white');
|
|
});
|
|
});
|
|
|
|
describe('LoadingOverlay Component', () => {
|
|
it('renders overlay with message', () => {
|
|
render(<LoadingOverlay message="Saving changes..." />);
|
|
expect(screen.getByText('Saving changes...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders with different backdrop styles', () => {
|
|
const { rerender, container } = render(<LoadingOverlay backdrop="light" />);
|
|
let overlay = container.querySelector('.fixed');
|
|
expect(overlay).toHaveClass('bg-white/80');
|
|
|
|
rerender(<LoadingOverlay backdrop="dark" />);
|
|
overlay = container.querySelector('.fixed');
|
|
expect(overlay).toHaveClass('bg-slate-900/80');
|
|
|
|
rerender(<LoadingOverlay backdrop="blur" />);
|
|
overlay = container.querySelector('.fixed');
|
|
expect(overlay).toHaveClass('backdrop-blur-sm');
|
|
});
|
|
|
|
it('renders as fixed overlay', () => {
|
|
const { container } = render(<LoadingOverlay />);
|
|
const overlay = container.querySelector('.fixed');
|
|
expect(overlay).toHaveClass('inset-0', 'z-50');
|
|
});
|
|
});
|
|
|
|
describe('InlineLoader Component', () => {
|
|
it('renders inline spinner', () => {
|
|
render(<InlineLoader />);
|
|
const spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('inline-block');
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
render(<InlineLoader className="ml-2" />);
|
|
const spinner = screen.getByRole('status');
|
|
expect(spinner).toHaveClass('ml-2');
|
|
});
|
|
});
|
|
|
|
describe('PageLoader Component', () => {
|
|
it('renders page loader', () => {
|
|
render(<PageLoader />);
|
|
const spinner = screen.getByRole('status');
|
|
expect(spinner).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders with message', () => {
|
|
render(<PageLoader message="Loading dashboard..." />);
|
|
expect(screen.getByText('Loading dashboard...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('has minimum height', () => {
|
|
const { container } = render(<PageLoader />);
|
|
const wrapper = container.firstChild as HTMLElement;
|
|
expect(wrapper).toHaveClass('min-h-[400px]');
|
|
});
|
|
});
|