- Introduced `useAdminStats`, `useAdminUsers`, and `useAdminOrganizations` hooks for admin data fetching with React Query. - Added `AdminSidebar`, `Breadcrumbs`, and related navigation components for the admin section. - Implemented comprehensive unit and integration tests for admin components. - Created E2E tests for admin access control, navigation, and dashboard functionality. - Updated exports to include new admin components.
325 lines
9.6 KiB
TypeScript
325 lines
9.6 KiB
TypeScript
/**
|
|
* Tests for StatCard Component
|
|
* Verifies stat display, loading states, and trend indicators
|
|
*/
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import { StatCard } from '@/components/admin/StatCard';
|
|
import { Users, Activity, Building2, FileText } from 'lucide-react';
|
|
|
|
describe('StatCard', () => {
|
|
const defaultProps = {
|
|
title: 'Total Users',
|
|
value: 1234,
|
|
icon: Users,
|
|
};
|
|
|
|
describe('Rendering', () => {
|
|
it('renders stat card with test id', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
expect(screen.getByTestId('stat-card')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders title correctly', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
expect(screen.getByTestId('stat-title')).toHaveTextContent('Total Users');
|
|
});
|
|
|
|
it('renders numeric value correctly', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('1234');
|
|
});
|
|
|
|
it('renders string value correctly', () => {
|
|
render(<StatCard {...defaultProps} value="Active" />);
|
|
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('Active');
|
|
});
|
|
|
|
it('renders icon', () => {
|
|
const { container } = render(<StatCard {...defaultProps} />);
|
|
|
|
// Icon should be rendered (lucide icons render as SVG)
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders description when provided', () => {
|
|
render(
|
|
<StatCard {...defaultProps} description="Total registered users" />
|
|
);
|
|
|
|
expect(screen.getByTestId('stat-description')).toHaveTextContent(
|
|
'Total registered users'
|
|
);
|
|
});
|
|
|
|
it('does not render description when not provided', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
expect(screen.queryByTestId('stat-description')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Loading State', () => {
|
|
it('applies loading class when loading', () => {
|
|
render(<StatCard {...defaultProps} loading />);
|
|
|
|
const card = screen.getByTestId('stat-card');
|
|
expect(card).toHaveClass('animate-pulse');
|
|
});
|
|
|
|
it('shows skeleton for value when loading', () => {
|
|
render(<StatCard {...defaultProps} loading />);
|
|
|
|
// Value should not be visible
|
|
expect(screen.queryByTestId('stat-value')).not.toBeInTheDocument();
|
|
|
|
// Skeleton placeholder should be present
|
|
const card = screen.getByTestId('stat-card');
|
|
const skeleton = card.querySelector('.bg-muted.rounded');
|
|
expect(skeleton).toBeInTheDocument();
|
|
});
|
|
|
|
it('hides description when loading', () => {
|
|
render(
|
|
<StatCard
|
|
{...defaultProps}
|
|
description="Test description"
|
|
loading
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByTestId('stat-description')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('hides trend when loading', () => {
|
|
render(
|
|
<StatCard
|
|
{...defaultProps}
|
|
trend={{ value: 10, label: 'vs last month', isPositive: true }}
|
|
loading
|
|
/>
|
|
);
|
|
|
|
expect(screen.queryByTestId('stat-trend')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('applies muted styles to icon when loading', () => {
|
|
const { container } = render(<StatCard {...defaultProps} loading />);
|
|
|
|
const icon = container.querySelector('svg');
|
|
expect(icon).toHaveClass('text-muted-foreground');
|
|
});
|
|
});
|
|
|
|
describe('Trend Indicator', () => {
|
|
it('renders positive trend correctly', () => {
|
|
render(
|
|
<StatCard
|
|
{...defaultProps}
|
|
trend={{ value: 12.5, label: 'vs last month', isPositive: true }}
|
|
/>
|
|
);
|
|
|
|
const trend = screen.getByTestId('stat-trend');
|
|
expect(trend).toBeInTheDocument();
|
|
expect(trend).toHaveTextContent('↑');
|
|
expect(trend).toHaveTextContent('12.5%');
|
|
expect(trend).toHaveTextContent('vs last month');
|
|
expect(trend).toHaveClass('text-green-600');
|
|
});
|
|
|
|
it('renders negative trend correctly', () => {
|
|
render(
|
|
<StatCard
|
|
{...defaultProps}
|
|
trend={{ value: 8.3, label: 'vs last week', isPositive: false }}
|
|
/>
|
|
);
|
|
|
|
const trend = screen.getByTestId('stat-trend');
|
|
expect(trend).toBeInTheDocument();
|
|
expect(trend).toHaveTextContent('↓');
|
|
expect(trend).toHaveTextContent('8.3%');
|
|
expect(trend).toHaveTextContent('vs last week');
|
|
expect(trend).toHaveClass('text-red-600');
|
|
});
|
|
|
|
it('handles negative trend values with absolute value', () => {
|
|
render(
|
|
<StatCard
|
|
{...defaultProps}
|
|
trend={{ value: -5.0, label: 'vs last month', isPositive: false }}
|
|
/>
|
|
);
|
|
|
|
const trend = screen.getByTestId('stat-trend');
|
|
// Should display absolute value
|
|
expect(trend).toHaveTextContent('5%');
|
|
expect(trend).not.toHaveTextContent('-5%');
|
|
});
|
|
|
|
it('does not render trend when not provided', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
expect(screen.queryByTestId('stat-trend')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Icon Variations', () => {
|
|
it('renders Users icon', () => {
|
|
const { container } = render(<StatCard {...defaultProps} icon={Users} />);
|
|
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders Activity icon', () => {
|
|
const { container } = render(
|
|
<StatCard {...defaultProps} icon={Activity} />
|
|
);
|
|
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders Building2 icon', () => {
|
|
const { container } = render(
|
|
<StatCard {...defaultProps} icon={Building2} />
|
|
);
|
|
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders FileText icon', () => {
|
|
const { container } = render(
|
|
<StatCard {...defaultProps} icon={FileText} />
|
|
);
|
|
|
|
const svg = container.querySelector('svg');
|
|
expect(svg).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Styling', () => {
|
|
it('applies custom className', () => {
|
|
render(<StatCard {...defaultProps} className="custom-class" />);
|
|
|
|
const card = screen.getByTestId('stat-card');
|
|
expect(card).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('applies default card styles', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
const card = screen.getByTestId('stat-card');
|
|
expect(card).toHaveClass('rounded-lg');
|
|
expect(card).toHaveClass('border');
|
|
expect(card).toHaveClass('bg-card');
|
|
expect(card).toHaveClass('p-6');
|
|
expect(card).toHaveClass('shadow-sm');
|
|
});
|
|
|
|
it('applies primary color to icon by default', () => {
|
|
const { container } = render(<StatCard {...defaultProps} />);
|
|
|
|
const icon = container.querySelector('svg');
|
|
expect(icon).toHaveClass('text-primary');
|
|
});
|
|
|
|
it('applies correct icon background', () => {
|
|
const { container } = render(<StatCard {...defaultProps} />);
|
|
|
|
const iconWrapper = container.querySelector('.rounded-full');
|
|
expect(iconWrapper).toHaveClass('bg-primary/10');
|
|
});
|
|
|
|
it('applies muted styles when loading', () => {
|
|
const { container } = render(<StatCard {...defaultProps} loading />);
|
|
|
|
const iconWrapper = container.querySelector('.rounded-full');
|
|
expect(iconWrapper).toHaveClass('bg-muted');
|
|
});
|
|
});
|
|
|
|
describe('Complex Scenarios', () => {
|
|
it('renders all props together', () => {
|
|
render(
|
|
<StatCard
|
|
title="Active Users"
|
|
value={856}
|
|
icon={Activity}
|
|
description="Currently online"
|
|
trend={{ value: 15.2, label: 'vs yesterday', isPositive: true }}
|
|
className="custom-stat"
|
|
/>
|
|
);
|
|
|
|
expect(screen.getByTestId('stat-title')).toHaveTextContent('Active Users');
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('856');
|
|
expect(screen.getByTestId('stat-description')).toHaveTextContent(
|
|
'Currently online'
|
|
);
|
|
expect(screen.getByTestId('stat-trend')).toHaveTextContent('↑');
|
|
expect(screen.getByTestId('stat-card')).toHaveClass('custom-stat');
|
|
});
|
|
|
|
it('handles zero value', () => {
|
|
render(<StatCard {...defaultProps} value={0} />);
|
|
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('0');
|
|
});
|
|
|
|
it('handles very large numbers', () => {
|
|
render(<StatCard {...defaultProps} value={1234567890} />);
|
|
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('1234567890');
|
|
});
|
|
|
|
it('handles formatted string values', () => {
|
|
render(<StatCard {...defaultProps} value="1,234" />);
|
|
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('1,234');
|
|
});
|
|
|
|
it('handles percentage string values', () => {
|
|
render(<StatCard {...defaultProps} value="98.5%" />);
|
|
|
|
expect(screen.getByTestId('stat-value')).toHaveTextContent('98.5%');
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('renders semantic HTML structure', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
const card = screen.getByTestId('stat-card');
|
|
expect(card.tagName).toBe('DIV');
|
|
});
|
|
|
|
it('maintains readable text contrast', () => {
|
|
render(<StatCard {...defaultProps} />);
|
|
|
|
const title = screen.getByTestId('stat-title');
|
|
expect(title).toHaveClass('text-muted-foreground');
|
|
|
|
const value = screen.getByTestId('stat-value');
|
|
expect(value).toHaveClass('font-bold');
|
|
});
|
|
|
|
it('renders description with appropriate text size', () => {
|
|
render(
|
|
<StatCard {...defaultProps} description="Test description" />
|
|
);
|
|
|
|
const description = screen.getByTestId('stat-description');
|
|
expect(description).toHaveClass('text-xs');
|
|
});
|
|
});
|
|
});
|