learning_ai_common_plat/packages/dashboard-components/src/components.test.tsx
2026-03-19 21:25:30 -07:00

254 lines
8.5 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { LoadingSpinner } from './LoadingSpinner.js';
import { LoadingSkeleton } from './LoadingSkeleton.js';
import { EmptyState } from './EmptyState.js';
import { PageHeader } from './PageHeader.js';
import { ErrorPage } from './ErrorPage.js';
import { NotFoundPage } from './NotFoundPage.js';
describe('LoadingSpinner', () => {
it('renders with default size', () => {
render(<LoadingSpinner />);
const status = screen.getByRole('status');
expect(status).toBeDefined();
expect(status.className).toContain('w-8 h-8');
});
it('renders with small size', () => {
render(<LoadingSpinner size="sm" />);
const status = screen.getByRole('status');
expect(status.className).toContain('w-4 h-4');
});
it('renders with large size', () => {
render(<LoadingSpinner size="lg" />);
const status = screen.getByRole('status');
expect(status.className).toContain('w-12 h-12');
});
it('applies custom className', () => {
render(<LoadingSpinner className="mt-4" />);
const status = screen.getByRole('status');
expect(status.className).toContain('mt-4');
});
it('renders SVG spinner element', () => {
render(<LoadingSpinner />);
const svg = screen.getByRole('status').querySelector('svg');
expect(svg).toBeDefined();
expect(svg!.classList.contains('animate-spin')).toBe(true);
});
});
describe('LoadingSkeleton', () => {
it('renders default 3 rows', () => {
render(<LoadingSkeleton />);
const container = screen.getByRole('status');
const rows = container.querySelectorAll('.animate-pulse');
expect(rows.length).toBe(3);
});
it('renders custom number of rows', () => {
render(<LoadingSkeleton rows={5} />);
const container = screen.getByRole('status');
const rows = container.querySelectorAll('.animate-pulse');
expect(rows.length).toBe(5);
});
it('applies custom className', () => {
render(<LoadingSkeleton className="my-8" />);
const container = screen.getByRole('status');
expect(container.className).toContain('my-8');
});
it('renders pulse-animated skeleton rows', () => {
render(<LoadingSkeleton rows={1} />);
const row = screen.getByRole('status').querySelector('.animate-pulse');
expect(row).toBeDefined();
expect(row!.classList.contains('rounded')).toBe(true);
});
});
describe('EmptyState', () => {
it('renders title and description', () => {
render(<EmptyState title="No items" description="Create your first item." />);
expect(screen.getByText('No items')).toBeDefined();
expect(screen.getByText('Create your first item.')).toBeDefined();
});
it('renders icon when provided', () => {
render(
<EmptyState
title="No items"
description="Create one."
icon={<span data-testid="icon">X</span>}
/>
);
expect(screen.getByTestId('icon')).toBeDefined();
});
it('does not render icon container when not provided', () => {
const { container } = render(<EmptyState title="No items" description="Create one." />);
const iconWrapper = container.querySelector('.w-16.h-16');
expect(iconWrapper).toBeNull();
});
it('renders action button and handles click', () => {
const onClick = vi.fn();
render(
<EmptyState
title="No items"
description="Create one."
action={{ label: 'Create', onClick }}
/>
);
const button = screen.getByText('Create');
expect(button).toBeDefined();
fireEvent.click(button);
expect(onClick).toHaveBeenCalledOnce();
});
it('does not render action button when not provided', () => {
const { container } = render(<EmptyState title="No items" description="Create one." />);
const buttons = container.querySelectorAll('button');
expect(buttons.length).toBe(0);
});
it('renders with theme-aware structure', () => {
const { container } = render(<EmptyState title="Test" description="Desc" />);
const heading = container.querySelector('h3');
expect(heading).toBeDefined();
expect(heading!.textContent).toBe('Test');
const desc = container.querySelector('p');
expect(desc).toBeDefined();
expect(desc!.textContent).toBe('Desc');
});
});
describe('PageHeader', () => {
it('renders title', () => {
render(<PageHeader title="Dashboard" />);
expect(screen.getByText('Dashboard')).toBeDefined();
});
it('renders breadcrumbs', () => {
render(
<PageHeader title="Users" breadcrumbs={[{ label: 'Home', href: '/' }, { label: 'Users' }]} />
);
expect(screen.getByText('Home')).toBeDefined();
expect(screen.getByLabelText('Breadcrumb')).toBeDefined();
});
it('renders breadcrumb links with href', () => {
render(
<PageHeader
title="Detail"
breadcrumbs={[{ label: 'Home', href: '/' }, { label: 'Detail' }]}
/>
);
const link = screen.getByText('Home');
expect(link.tagName).toBe('A');
expect(link.getAttribute('href')).toBe('/');
});
it('renders breadcrumb text without href', () => {
render(<PageHeader title="Detail" breadcrumbs={[{ label: 'Current' }]} />);
const text = screen.getByText('Current');
expect(text.tagName).toBe('SPAN');
});
it('renders actions', () => {
render(<PageHeader title="Test" actions={<button data-testid="action-btn">Action</button>} />);
expect(screen.getByTestId('action-btn')).toBeDefined();
});
it('does not render breadcrumb nav when empty', () => {
const { container } = render(<PageHeader title="Simple" />);
const nav = container.querySelector('nav');
expect(nav).toBeNull();
});
});
describe('ErrorPage', () => {
it('renders with default props', () => {
render(<ErrorPage />);
expect(screen.getByText('Something went wrong')).toBeDefined();
expect(screen.getByText('An unexpected error occurred. Please try again.')).toBeDefined();
});
it('renders custom title and message', () => {
render(<ErrorPage title="Server Error" message="The server is down." />);
expect(screen.getByText('Server Error')).toBeDefined();
expect(screen.getByText('The server is down.')).toBeDefined();
});
it('renders retry button and handles click', () => {
const onRetry = vi.fn();
render(<ErrorPage onRetry={onRetry} />);
const button = screen.getByText('Try Again');
expect(button).toBeDefined();
fireEvent.click(button);
expect(onRetry).toHaveBeenCalledOnce();
});
it('does not render retry button when not provided', () => {
const { container } = render(<ErrorPage />);
const buttons = container.querySelectorAll('button');
expect(buttons.length).toBe(0);
});
it('renders error icon and semantic structure', () => {
const { container } = render(<ErrorPage />);
const iconContainer = container.querySelector('.w-16');
expect(iconContainer).toBeDefined();
const svg = iconContainer!.querySelector('svg');
expect(svg).toBeDefined();
const heading = container.querySelector('h2');
expect(heading).toBeDefined();
expect(heading!.textContent).toBe('Something went wrong');
});
});
describe('NotFoundPage', () => {
it('renders with default props', () => {
render(<NotFoundPage />);
expect(screen.getByText('404')).toBeDefined();
expect(screen.getByText('Page not found')).toBeDefined();
});
it('renders custom status code', () => {
render(<NotFoundPage statusCode="403" title="Forbidden" />);
expect(screen.getByText('403')).toBeDefined();
expect(screen.getByText('Forbidden')).toBeDefined();
});
it('renders back button with onClick', () => {
const onBack = vi.fn();
render(<NotFoundPage onBack={onBack} />);
const button = screen.getByText('Go Back');
fireEvent.click(button);
expect(onBack).toHaveBeenCalledOnce();
});
it('renders back link with href', () => {
render(<NotFoundPage backHref="/" backLabel="Go Home" />);
const link = screen.getByText('Go Home');
expect(link.tagName).toBe('A');
expect(link.getAttribute('href')).toBe('/');
});
it('does not render button when neither onBack nor backHref provided', () => {
const { container } = render(<NotFoundPage />);
const buttons = container.querySelectorAll('button');
const links = container.querySelectorAll('a');
expect(buttons.length).toBe(0);
expect(links.length).toBe(0);
});
it('custom backLabel is used', () => {
render(<NotFoundPage onBack={() => {}} backLabel="Return" />);
expect(screen.getByText('Return')).toBeDefined();
});
});