Sergei 4b60a92777 Add basic UI components for web application
- Set up Tailwind CSS configuration for styling
- Create Button component with variants (primary, secondary, outline, ghost, danger)
- Create Input component with label, error, and helper text support
- Create Card component with composable subcomponents (Header, Title, Description, Content, Footer)
- Create LoadingSpinner component with size and fullscreen options
- Create ErrorMessage component with retry and dismiss actions
- Add comprehensive test suite using Jest and React Testing Library
- Configure ESLint and Jest for quality assurance

All components follow consistent design patterns from mobile app
and include proper TypeScript types and accessibility features.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 18:11:31 -08:00

142 lines
4.7 KiB
TypeScript

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from '../Card';
describe('Card', () => {
it('renders children correctly', () => {
render(<Card>Card content</Card>);
expect(screen.getByText('Card content')).toBeInTheDocument();
});
it('renders with base card styling', () => {
const { container } = render(<Card>Content</Card>);
const card = container.firstChild as HTMLElement;
expect(card.className).toContain('bg-white');
expect(card.className).toContain('rounded-lg');
});
it('applies padding classes based on padding prop', () => {
const { container: container1 } = render(<Card padding="sm">Content</Card>);
expect((container1.firstChild as HTMLElement).className).toContain('p-3');
const { container: container2 } = render(<Card padding="md">Content</Card>);
expect((container2.firstChild as HTMLElement).className).toContain('p-4');
const { container: container3 } = render(<Card padding="lg">Content</Card>);
expect((container3.firstChild as HTMLElement).className).toContain('p-6');
});
it('applies hover class when hover prop is true', () => {
const { container } = render(<Card hover>Hoverable card</Card>);
const card = container.firstChild as HTMLElement;
expect(card.className).toContain('hover:shadow-md');
});
it('renders as button role when onClick is provided', () => {
const handleClick = jest.fn();
render(<Card onClick={handleClick}>Clickable card</Card>);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Card onClick={handleClick}>Clickable card</Card>);
const card = screen.getByRole('button');
fireEvent.click(card);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies custom className', () => {
const { container } = render(<Card className="custom-class">Content</Card>);
const card = container.firstChild as HTMLElement;
expect(card.className).toContain('custom-class');
});
});
describe('CardHeader', () => {
it('renders children correctly', () => {
render(<CardHeader>Header content</CardHeader>);
expect(screen.getByText('Header content')).toBeInTheDocument();
});
it('applies border bottom styling', () => {
const { container } = render(<CardHeader>Header</CardHeader>);
const header = container.firstChild as HTMLElement;
expect(header.className).toContain('border-b');
});
});
describe('CardTitle', () => {
it('renders title correctly', () => {
render(<CardTitle>Card Title</CardTitle>);
expect(screen.getByText('Card Title')).toBeInTheDocument();
});
it('renders as h3 element', () => {
render(<CardTitle>Title</CardTitle>);
const title = screen.getByText('Title');
expect(title.tagName).toBe('H3');
});
});
describe('CardDescription', () => {
it('renders description correctly', () => {
render(<CardDescription>Card description</CardDescription>);
expect(screen.getByText('Card description')).toBeInTheDocument();
});
it('applies text styling', () => {
render(<CardDescription>Description</CardDescription>);
const description = screen.getByText('Description');
expect(description).toHaveClass('text-sm');
expect(description).toHaveClass('text-textSecondary');
});
});
describe('CardContent', () => {
it('renders content correctly', () => {
render(<CardContent>Content area</CardContent>);
expect(screen.getByText('Content area')).toBeInTheDocument();
});
});
describe('CardFooter', () => {
it('renders footer correctly', () => {
render(<CardFooter>Footer content</CardFooter>);
expect(screen.getByText('Footer content')).toBeInTheDocument();
});
it('applies border top styling', () => {
const { container } = render(<CardFooter>Footer</CardFooter>);
const footer = container.firstChild as HTMLElement;
expect(footer.className).toContain('border-t');
});
});
describe('Card composition', () => {
it('renders complete card with all components', () => {
render(
<Card>
<CardHeader>
<CardTitle>Test Card</CardTitle>
<CardDescription>This is a test card</CardDescription>
</CardHeader>
<CardContent>Main content</CardContent>
<CardFooter>Footer actions</CardFooter>
</Card>
);
expect(screen.getByText('Test Card')).toBeInTheDocument();
expect(screen.getByText('This is a test card')).toBeInTheDocument();
expect(screen.getByText('Main content')).toBeInTheDocument();
expect(screen.getByText('Footer actions')).toBeInTheDocument();
});
});