WellNuo/web/components/ui/__tests__/ErrorMessage.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

207 lines
6.1 KiB
TypeScript

import { render, screen, fireEvent } from '@testing-library/react';
import {
ErrorMessage,
FullScreenError,
FieldError,
EmptyState,
} from '../ErrorMessage';
describe('ErrorMessage Component', () => {
it('renders error message', () => {
render(<ErrorMessage message="Something went wrong" />);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('renders with different severity levels', () => {
const { rerender, container } = render(
<ErrorMessage message="Error" severity="error" />
);
let alert = container.querySelector('[role="alert"]');
expect(alert).toHaveClass('bg-red-50', 'border-red-200');
rerender(<ErrorMessage message="Warning" severity="warning" />);
alert = container.querySelector('[role="alert"]');
expect(alert).toHaveClass('bg-yellow-50', 'border-yellow-200');
rerender(<ErrorMessage message="Info" severity="info" />);
alert = container.querySelector('[role="alert"]');
expect(alert).toHaveClass('bg-blue-50', 'border-blue-200');
});
it('calls onRetry when retry button is clicked', () => {
const handleRetry = jest.fn();
render(<ErrorMessage message="Failed to load" onRetry={handleRetry} />);
const retryButton = screen.getByRole('button', { name: /retry/i });
fireEvent.click(retryButton);
expect(handleRetry).toHaveBeenCalledTimes(1);
});
it('calls onDismiss when dismiss button is clicked', () => {
const handleDismiss = jest.fn();
render(<ErrorMessage message="Error" onDismiss={handleDismiss} />);
const dismissButton = screen.getByLabelText('Dismiss');
fireEvent.click(dismissButton);
expect(handleDismiss).toHaveBeenCalledTimes(1);
});
it('renders without action buttons', () => {
render(<ErrorMessage message="Error" />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('has proper ARIA attributes', () => {
render(<ErrorMessage message="Error occurred" />);
const alert = screen.getByRole('alert');
expect(alert).toHaveAttribute('aria-live', 'polite');
});
});
describe('FullScreenError Component', () => {
it('renders full screen error with title and message', () => {
render(
<FullScreenError
title="Page Not Found"
message="The page you are looking for does not exist."
/>
);
expect(screen.getByText('Page Not Found')).toBeInTheDocument();
expect(
screen.getByText('The page you are looking for does not exist.')
).toBeInTheDocument();
});
it('uses default title when not provided', () => {
render(<FullScreenError message="An error occurred" />);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('calls onRetry when retry button is clicked', () => {
const handleRetry = jest.fn();
render(
<FullScreenError
message="Failed to load"
onRetry={handleRetry}
/>
);
const retryButton = screen.getByRole('button', { name: /try again/i });
fireEvent.click(retryButton);
expect(handleRetry).toHaveBeenCalledTimes(1);
});
it('calls onBack when back button is clicked', () => {
const handleBack = jest.fn();
render(
<FullScreenError
message="Error"
onBack={handleBack}
/>
);
const backButton = screen.getByRole('button', { name: /go back/i });
fireEvent.click(backButton);
expect(handleBack).toHaveBeenCalledTimes(1);
});
it('renders with custom button labels', () => {
render(
<FullScreenError
message="Error"
onRetry={() => {}}
onBack={() => {}}
retryLabel="Reload"
backLabel="Return"
/>
);
expect(screen.getByRole('button', { name: /reload/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /return/i })).toBeInTheDocument();
});
it('renders different icon types', () => {
const { container } = render(
<FullScreenError message="Error" icon="offline" />
);
// Check that SVG is rendered
expect(container.querySelector('svg')).toBeInTheDocument();
});
});
describe('FieldError Component', () => {
it('renders field error message', () => {
render(<FieldError message="Email is required" />);
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
it('has proper ARIA role', () => {
render(<FieldError message="Invalid input" />);
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
});
it('renders with icon', () => {
const { container } = render(<FieldError message="Error" />);
const svg = container.querySelector('svg');
expect(svg).toBeInTheDocument();
});
it('applies custom className', () => {
render(<FieldError message="Error" className="mb-4" />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('mb-4');
});
});
describe('EmptyState Component', () => {
it('renders empty state with title', () => {
render(<EmptyState title="No items found" />);
expect(screen.getByText('No items found')).toBeInTheDocument();
});
it('renders with message', () => {
render(
<EmptyState
title="No data"
message="Get started by adding your first item"
/>
);
expect(
screen.getByText('Get started by adding your first item')
).toBeInTheDocument();
});
it('renders action button when provided', () => {
const handleAction = jest.fn();
render(
<EmptyState
title="Empty"
actionLabel="Add Item"
onAction={handleAction}
/>
);
const button = screen.getByRole('button', { name: /add item/i });
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(handleAction).toHaveBeenCalledTimes(1);
});
it('renders custom icon', () => {
const icon = <div data-testid="custom-icon">Icon</div>;
render(<EmptyState title="Empty" icon={icon} />);
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
});
it('does not render action button when not provided', () => {
render(<EmptyState title="Empty" />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});