WellNuo/web/components/ui/__tests__/LoadingSpinner.test.tsx
Sergei 71f194cc4d Add Loading & Error UI components for web application
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>
2026-01-31 18:26:28 -08:00

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]');
});
});