Add comprehensive unit tests for homepage components and utilities

- Introduced unit tests for homepage components: `QuickStartCode`, `Header`, `DemoCredentialsModal`, `AnimatedTerminal`, `CTASection`, and `StatsSection`.
- Added utility tests for `chart-colors` including opacity, palettes, and gradient validation.
- Mocked dependencies (`framer-motion`, `react-syntax-highlighter`, `DemoCredentialsModal`) for isolated testing.
- Verified accessibility features, animations, and interactive behaviors across components.
This commit is contained in:
2025-11-08 17:06:14 +01:00
parent fe289228e1
commit b630559e0b
14 changed files with 1603 additions and 37 deletions

View File

@@ -0,0 +1,65 @@
/**
* Tests for Admin Organization Members Page
*/
import { render, screen } from '@testing-library/react';
import OrganizationMembersPage from '@/app/admin/organizations/[id]/members/page';
// Mock Next.js Link
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
// Mock OrganizationMembersContent component
jest.mock('@/components/admin/organizations/OrganizationMembersContent', () => ({
OrganizationMembersContent: ({ organizationId }: { organizationId: string }) => (
<div data-testid="organization-members-content">
Organization ID: {organizationId}
</div>
),
}));
describe('OrganizationMembersPage', () => {
it('renders back button to organizations', async () => {
const params = Promise.resolve({ id: 'org-123' });
render(await OrganizationMembersPage({ params }));
const backLink = screen.getByRole('link');
expect(backLink).toHaveAttribute('href', '/admin/organizations');
});
it('renders organization members content with correct ID', async () => {
const params = Promise.resolve({ id: 'org-456' });
render(await OrganizationMembersPage({ params }));
expect(screen.getByTestId('organization-members-content')).toBeInTheDocument();
expect(screen.getByText('Organization ID: org-456')).toBeInTheDocument();
});
it('renders container with proper styling', async () => {
const params = Promise.resolve({ id: 'org-789' });
const { container } = render(await OrganizationMembersPage({ params }));
const mainContainer = container.querySelector('.container');
expect(mainContainer).toBeInTheDocument();
});
it('handles different organization IDs', async () => {
const testIds = ['org-001', 'org-abc-123', 'test-org'];
for (const testId of testIds) {
const params = Promise.resolve({ id: testId });
const { unmount } = render(await OrganizationMembersPage({ params }));
expect(screen.getByText(`Organization ID: ${testId}`)).toBeInTheDocument();
unmount();
}
});
});

View File

@@ -1,12 +1,12 @@
/**
* Tests for Home Page
* Smoke tests for static content
* Tests for the new FastNext Template landing page
*/
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import Home from '@/app/page';
// Mock Next.js Image component
// Mock Next.js components
jest.mock('next/image', () => ({
__esModule: true,
default: (props: any) => {
@@ -15,56 +15,241 @@ jest.mock('next/image', () => ({
},
}));
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
// Mock framer-motion to avoid animation issues in tests
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
h1: ({ children, ...props }: any) => <h1 {...props}>{children}</h1>,
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
section: ({ children, ...props }: any) => <section {...props}>{children}</section>,
},
AnimatePresence: ({ children }: any) => <>{children}</>,
useInView: () => true, // Always in view for tests
}));
// Mock react-syntax-highlighter to avoid ESM issues
jest.mock('react-syntax-highlighter', () => ({
Prism: ({ children, ...props }: any) => <pre {...props}>{children}</pre>,
}));
jest.mock('react-syntax-highlighter/dist/esm/styles/prism', () => ({
vscDarkPlus: {},
}));
describe('HomePage', () => {
it('renders without crashing', () => {
render(<Home />);
expect(screen.getByText(/get started by editing/i)).toBeInTheDocument();
describe('Page Structure', () => {
it('renders without crashing', () => {
render(<Home />);
expect(screen.getByRole('banner')).toBeInTheDocument(); // header
expect(screen.getByRole('main')).toBeInTheDocument();
expect(screen.getByRole('contentinfo')).toBeInTheDocument(); // footer
});
it('renders header with logo', () => {
render(<Home />);
const header = screen.getByRole('banner');
expect(within(header).getByText('FastNext')).toBeInTheDocument();
expect(within(header).getByText('Template')).toBeInTheDocument();
});
it('renders footer with copyright', () => {
render(<Home />);
const footer = screen.getByRole('contentinfo');
expect(within(footer).getByText(/FastNext Template. MIT Licensed/i)).toBeInTheDocument();
});
});
it('renders Next.js logo', () => {
render(<Home />);
describe('Hero Section', () => {
it('renders main headline', () => {
render(<Home />);
expect(screen.getAllByText(/Everything You Need to Build/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/Modern Web Applications/i)[0]).toBeInTheDocument();
});
const logo = screen.getByAltText('Next.js logo');
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute('src', '/next.svg');
it('renders production-ready messaging', () => {
render(<Home />);
expect(screen.getByText(/Production-ready FastAPI/i)).toBeInTheDocument();
});
it('renders test coverage stats', () => {
render(<Home />);
const coverageTexts = screen.getAllByText('97%');
expect(coverageTexts.length).toBeGreaterThan(0);
expect(screen.getAllByText(/Test Coverage/i)[0]).toBeInTheDocument();
const testCountTexts = screen.getAllByText('743');
expect(testCountTexts.length).toBeGreaterThan(0);
expect(screen.getAllByText(/Passing Tests/i)[0]).toBeInTheDocument();
});
});
it('renders Vercel logo', () => {
render(<Home />);
describe('Context Section', () => {
it('renders what you get message', () => {
render(<Home />);
expect(screen.getByText(/What You Get Out of the Box/i)).toBeInTheDocument();
});
const logo = screen.getByAltText('Vercel logomark');
expect(logo).toBeInTheDocument();
expect(logo).toHaveAttribute('src', '/vercel.svg');
it('renders key features', () => {
render(<Home />);
expect(screen.getAllByText(/Clone & Deploy in < 5 minutes/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/97% Test Coverage \(743 tests\)/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/12\+ Documentation Guides/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/Zero Commercial Dependencies/i)[0]).toBeInTheDocument();
});
});
it('has correct external links', () => {
render(<Home />);
describe('Feature Grid', () => {
it('renders comprehensive features heading', () => {
render(<Home />);
expect(screen.getByText(/Comprehensive Features, No Assembly Required/i)).toBeInTheDocument();
});
const deployLink = screen.getByRole('link', { name: /deploy now/i });
expect(deployLink).toHaveAttribute('href', expect.stringContaining('vercel.com'));
expect(deployLink).toHaveAttribute('target', '_blank');
expect(deployLink).toHaveAttribute('rel', 'noopener noreferrer');
it('renders all 6 feature cards', () => {
render(<Home />);
expect(screen.getAllByText('Authentication & Security')[0]).toBeInTheDocument();
expect(screen.getAllByText('Multi-Tenant Organizations')[0]).toBeInTheDocument();
expect(screen.getAllByText('Admin Dashboard')[0]).toBeInTheDocument();
expect(screen.getAllByText('Complete Documentation')[0]).toBeInTheDocument();
expect(screen.getAllByText('Production Ready')[0]).toBeInTheDocument();
expect(screen.getAllByText('Developer Experience')[0]).toBeInTheDocument();
});
const docsLink = screen.getByRole('link', { name: /read our docs/i });
expect(docsLink).toHaveAttribute('href', expect.stringContaining('nextjs.org/docs'));
expect(docsLink).toHaveAttribute('target', '_blank');
it('has CTAs for each feature', () => {
render(<Home />);
expect(screen.getByRole('link', { name: /View Auth Flow/i })).toHaveAttribute('href', '/login');
expect(screen.getByRole('link', { name: /See Organizations/i })).toHaveAttribute('href', '/admin/organizations');
expect(screen.getByRole('link', { name: /Try Admin Panel/i })).toHaveAttribute('href', '/admin');
});
});
it('renders footer links', () => {
render(<Home />);
describe('Demo Section', () => {
it('renders demo section heading', () => {
render(<Home />);
expect(screen.getByText(/See It In Action/i)).toBeInTheDocument();
});
expect(screen.getByRole('link', { name: /learn/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /examples/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /go to nextjs\.org/i })).toBeInTheDocument();
it('renders demo cards', () => {
render(<Home />);
expect(screen.getAllByText('Component Showcase')[0]).toBeInTheDocument();
expect(screen.getAllByText('Authentication Flow')[0]).toBeInTheDocument();
// Admin Dashboard appears in both Feature Grid and Demo Section, so use getAllByText
const adminDashboards = screen.getAllByText('Admin Dashboard');
expect(adminDashboards.length).toBeGreaterThanOrEqual(1);
});
it('displays demo credentials', () => {
render(<Home />);
const credentials = screen.getAllByText(/Demo Credentials:/i);
expect(credentials.length).toBeGreaterThan(0);
});
});
it('has accessible image alt texts', () => {
render(<Home />);
describe('Tech Stack Section', () => {
it('renders tech stack heading', () => {
render(<Home />);
expect(screen.getByText(/Modern, Type-Safe, Production-Grade Stack/i)).toBeInTheDocument();
});
expect(screen.getByAltText('Next.js logo')).toBeInTheDocument();
expect(screen.getByAltText('Vercel logomark')).toBeInTheDocument();
expect(screen.getByAltText('File icon')).toBeInTheDocument();
expect(screen.getByAltText('Window icon')).toBeInTheDocument();
expect(screen.getByAltText('Globe icon')).toBeInTheDocument();
it('renders all technologies', () => {
render(<Home />);
expect(screen.getAllByText('FastAPI')[0]).toBeInTheDocument();
expect(screen.getAllByText('Next.js 15')[0]).toBeInTheDocument();
expect(screen.getAllByText('PostgreSQL')[0]).toBeInTheDocument();
expect(screen.getAllByText('TypeScript')[0]).toBeInTheDocument();
expect(screen.getAllByText('Docker')[0]).toBeInTheDocument();
expect(screen.getAllByText('TailwindCSS')[0]).toBeInTheDocument();
expect(screen.getAllByText(/shadcn\/ui/i)[0]).toBeInTheDocument();
expect(screen.getAllByText('Playwright')[0]).toBeInTheDocument();
});
});
describe('Philosophy Section', () => {
it('renders why this template exists', () => {
render(<Home />);
expect(screen.getByText(/Why This Template Exists/i)).toBeInTheDocument();
});
it('renders what you wont find section', () => {
render(<Home />);
expect(screen.getByText(/What You Won't Find Here/i)).toBeInTheDocument();
expect(screen.getAllByText(/Vendor lock-in/i)[0]).toBeInTheDocument();
});
it('renders what you will find section', () => {
render(<Home />);
expect(screen.getByText(/What You Will Find/i)).toBeInTheDocument();
expect(screen.getByText(/Production patterns that actually work/i)).toBeInTheDocument();
});
});
describe('Quick Start Section', () => {
it('renders quick start heading', () => {
render(<Home />);
expect(screen.getByText(/5-Minute Setup/i)).toBeInTheDocument();
});
});
describe('CTA Section', () => {
it('renders final CTA', () => {
render(<Home />);
expect(screen.getByText(/Start Building,/i)).toBeInTheDocument();
expect(screen.getByText(/Not Boilerplating/i)).toBeInTheDocument();
});
it('has GitHub link', () => {
render(<Home />);
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
expect(githubLinks.length).toBeGreaterThan(0);
expect(githubLinks[0]).toHaveAttribute('href', expect.stringContaining('github.com'));
});
});
describe('Navigation Links', () => {
it('has login link', () => {
render(<Home />);
const loginLinks = screen.getAllByRole('link', { name: /Login/i });
expect(loginLinks.some(link => link.getAttribute('href') === '/login')).toBe(true);
});
it('has component showcase link', () => {
render(<Home />);
const devLinks = screen.getAllByRole('link', { name: /Component/i });
expect(devLinks.some(link => link.getAttribute('href') === '/dev')).toBe(true);
});
it('has admin demo link', () => {
render(<Home />);
const adminLinks = screen.getAllByRole('link', { name: /Admin/i });
expect(adminLinks.some(link => link.getAttribute('href') === '/admin')).toBe(true);
});
});
describe('Accessibility', () => {
it('has proper heading hierarchy', () => {
render(<Home />);
const main = screen.getByRole('main');
const headings = within(main).getAllByRole('heading');
expect(headings.length).toBeGreaterThan(0);
});
it('has external links with proper attributes', () => {
render(<Home />);
const githubLinks = screen.getAllByRole('link', { name: /GitHub/i });
const externalLink = githubLinks.find(link =>
link.getAttribute('href')?.includes('github.com')
);
expect(externalLink).toHaveAttribute('target', '_blank');
expect(externalLink).toHaveAttribute('rel', 'noopener noreferrer');
});
});
});

View File

@@ -6,14 +6,33 @@ import { render, screen } from '@testing-library/react';
import { UserStatusChart } from '@/components/charts/UserStatusChart';
import type { UserStatusData } from '@/components/charts/UserStatusChart';
// Capture label function at module level for testing
let capturedLabelFunction: ((entry: any) => string) | null = null;
// Mock recharts to avoid rendering issues in tests
jest.mock('recharts', () => {
const OriginalModule = jest.requireActual('recharts');
const MockPie = (props: any) => {
// Capture the label function for testing
if (props.label && typeof props.label === 'function') {
capturedLabelFunction = props.label;
}
return <div data-testid="pie-chart">{props.children}</div>;
};
return {
...OriginalModule,
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
<div data-testid="responsive-container">{children}</div>
),
PieChart: ({ children }: { children: React.ReactNode }) => (
<div data-testid="pie-chart-container">{children}</div>
),
Pie: MockPie,
Cell: ({ fill }: { fill: string }) => <div data-testid="cell" style={{ fill }} />,
Tooltip: () => <div data-testid="tooltip" />,
Legend: () => <div data-testid="legend" />,
};
});
@@ -69,4 +88,57 @@ describe('UserStatusChart', () => {
expect(screen.getByText('User Status Distribution')).toBeInTheDocument();
expect(screen.getByTestId('responsive-container')).toBeInTheDocument();
});
describe('renderLabel function', () => {
beforeEach(() => {
capturedLabelFunction = null;
});
it('formats label with name and percentage', () => {
render(<UserStatusChart data={mockData} />);
expect(capturedLabelFunction).toBeTruthy();
if (capturedLabelFunction) {
const result = capturedLabelFunction({ name: 'Active', percent: 0.75 });
expect(result).toBe('Active: 75%');
}
});
it('formats label with zero percent', () => {
render(<UserStatusChart data={mockData} />);
if (capturedLabelFunction) {
const result = capturedLabelFunction({ name: 'Inactive', percent: 0 });
expect(result).toBe('Inactive: 0%');
}
});
it('formats label with 100 percent', () => {
render(<UserStatusChart data={mockData} />);
if (capturedLabelFunction) {
const result = capturedLabelFunction({ name: 'All Users', percent: 1 });
expect(result).toBe('All Users: 100%');
}
});
it('rounds percentage to nearest whole number', () => {
render(<UserStatusChart data={mockData} />);
if (capturedLabelFunction) {
const result = capturedLabelFunction({ name: 'Pending', percent: 0.4567 });
expect(result).toBe('Pending: 46%');
}
});
it('handles small percentages', () => {
render(<UserStatusChart data={mockData} />);
if (capturedLabelFunction) {
const result = capturedLabelFunction({ name: 'Suspended', percent: 0.025 });
expect(result).toBe('Suspended: 3%');
}
});
});
});

View File

@@ -0,0 +1,99 @@
/**
* Tests for AnimatedTerminal component
*/
import { render, screen } from '@testing-library/react';
import { AnimatedTerminal } from '@/components/home/AnimatedTerminal';
// Mock framer-motion
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}));
// Mock Next.js Link
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
// IntersectionObserver is already mocked in jest.setup.js
describe('AnimatedTerminal', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('renders the section heading', () => {
render(<AnimatedTerminal />);
expect(screen.getByText('Get Started in Seconds')).toBeInTheDocument();
expect(screen.getByText(/Clone, run, and start building/i)).toBeInTheDocument();
});
it('renders terminal window with header', () => {
render(<AnimatedTerminal />);
expect(screen.getByText('bash')).toBeInTheDocument();
});
it('renders Try Live Demo button', () => {
render(<AnimatedTerminal />);
const demoLink = screen.getByRole('link', { name: /try live demo/i });
expect(demoLink).toHaveAttribute('href', '/login');
});
it('displays message about trying demo', () => {
render(<AnimatedTerminal />);
expect(screen.getByText(/Or try the live demo without installing/i)).toBeInTheDocument();
});
it('starts animation when component mounts', () => {
render(<AnimatedTerminal />);
// Animation should start because IntersectionObserver mock triggers immediately
// Advance timers to show first command
jest.advanceTimersByTime(1000);
// Check if animated content appears (the mock renders all commands immediately in tests)
const terminalContent = screen.getByText('bash').parentElement?.parentElement;
expect(terminalContent).toBeInTheDocument();
});
it('renders terminal with proper structure', () => {
render(<AnimatedTerminal />);
// Verify terminal window has proper structure
const bashIndicator = screen.getByText('bash');
expect(bashIndicator).toBeInTheDocument();
expect(bashIndicator.parentElement).toBeInTheDocument();
});
describe('Accessibility', () => {
it('has descriptive text for screen readers', () => {
render(<AnimatedTerminal />);
expect(screen.getByText('Get Started in Seconds')).toBeInTheDocument();
});
it('has proper link to demo', () => {
render(<AnimatedTerminal />);
const demoLink = screen.getByRole('link', { name: /try live demo/i });
expect(demoLink).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,132 @@
/**
* Tests for CTASection component
*/
import { render, screen, fireEvent } from '@testing-library/react';
import { CTASection } from '@/components/home/CTASection';
// Mock framer-motion
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}));
// Mock Next.js Link
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
// Mock DemoCredentialsModal
jest.mock('@/components/home/DemoCredentialsModal', () => ({
DemoCredentialsModal: ({ open, onClose }: any) => (
open ? <div data-testid="demo-modal">
<button onClick={onClose}>Close Modal</button>
</div> : null
),
}));
describe('CTASection', () => {
it('renders main headline', () => {
render(<CTASection />);
expect(screen.getByText(/Start Building,/i)).toBeInTheDocument();
expect(screen.getByText(/Not Boilerplating/i)).toBeInTheDocument();
});
it('renders subtext with key messaging', () => {
render(<CTASection />);
expect(screen.getByText(/Clone the repository, read the docs/i)).toBeInTheDocument();
expect(screen.getByText(/Free forever, MIT licensed/i)).toBeInTheDocument();
});
it('renders GitHub CTA button', () => {
render(<CTASection />);
const githubLink = screen.getByRole('link', { name: /get started on github/i });
expect(githubLink).toHaveAttribute('href', 'https://github.com/your-org/fast-next-template');
expect(githubLink).toHaveAttribute('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
});
it('renders Try Live Demo button', () => {
render(<CTASection />);
const demoButton = screen.getByRole('button', { name: /try live demo/i });
expect(demoButton).toBeInTheDocument();
});
it('renders Read Documentation link', () => {
render(<CTASection />);
const docsLink = screen.getByRole('link', { name: /read documentation/i });
expect(docsLink).toHaveAttribute('href', 'https://github.com/your-org/fast-next-template#documentation');
expect(docsLink).toHaveAttribute('target', '_blank');
expect(docsLink).toHaveAttribute('rel', 'noopener noreferrer');
});
it('renders help text with internal links', () => {
render(<CTASection />);
expect(screen.getByText(/Need help getting started\?/i)).toBeInTheDocument();
const componentShowcaseLink = screen.getByRole('link', { name: /component showcase/i });
expect(componentShowcaseLink).toHaveAttribute('href', '/dev');
const adminDashboardLink = screen.getByRole('link', { name: /admin dashboard demo/i });
expect(adminDashboardLink).toHaveAttribute('href', '/admin');
});
it('opens demo modal when Try Live Demo button is clicked', () => {
render(<CTASection />);
const demoButton = screen.getByRole('button', { name: /try live demo/i });
fireEvent.click(demoButton);
expect(screen.getByTestId('demo-modal')).toBeInTheDocument();
});
it('closes demo modal when close is called', () => {
render(<CTASection />);
// Open modal
const demoButton = screen.getByRole('button', { name: /try live demo/i });
fireEvent.click(demoButton);
expect(screen.getByTestId('demo-modal')).toBeInTheDocument();
// Close modal
const closeButton = screen.getByText('Close Modal');
fireEvent.click(closeButton);
expect(screen.queryByTestId('demo-modal')).not.toBeInTheDocument();
});
describe('Accessibility', () => {
it('has proper external link attributes', () => {
render(<CTASection />);
const externalLinks = [
screen.getByRole('link', { name: /get started on github/i }),
screen.getByRole('link', { name: /read documentation/i }),
];
externalLinks.forEach(link => {
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
});
it('has descriptive button text', () => {
render(<CTASection />);
expect(screen.getByRole('button', { name: /try live demo/i })).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,170 @@
/**
* Tests for DemoCredentialsModal component
*/
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { DemoCredentialsModal } from '@/components/home/DemoCredentialsModal';
// Mock Next.js Link
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
describe('DemoCredentialsModal', () => {
const mockOnClose = jest.fn();
beforeEach(() => {
mockOnClose.mockClear();
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
},
});
});
it('renders when open is true', () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
expect(screen.getByText('Try the Live Demo')).toBeInTheDocument();
expect(screen.getByText(/Use these credentials to explore/i)).toBeInTheDocument();
});
it('does not render when open is false', () => {
render(<DemoCredentialsModal open={false} onClose={mockOnClose} />);
expect(screen.queryByText('Try the Live Demo')).not.toBeInTheDocument();
});
it('displays regular user credentials', () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
expect(screen.getByText('Regular User')).toBeInTheDocument();
expect(screen.getByText('demo@example.com')).toBeInTheDocument();
expect(screen.getByText('Demo123!')).toBeInTheDocument();
expect(screen.getByText(/Access settings, organizations/i)).toBeInTheDocument();
});
it('displays admin user credentials', () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
expect(screen.getByText('Admin User (Superuser)')).toBeInTheDocument();
expect(screen.getByText('admin@example.com')).toBeInTheDocument();
expect(screen.getByText('Admin123!')).toBeInTheDocument();
expect(screen.getByText(/Full admin panel access/i)).toBeInTheDocument();
});
it('copies regular user credentials to clipboard', async () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
const copyButtons = screen.getAllByRole('button');
const regularCopyButton = copyButtons.find(btn => btn.textContent?.includes('Copy'));
fireEvent.click(regularCopyButton!);
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('demo@example.com\nDemo123!');
const copiedButtons = screen.getAllByRole('button');
const copiedButton = copiedButtons.find(btn => btn.textContent?.includes('Copied!'));
expect(copiedButton).toBeInTheDocument();
});
});
it('copies admin user credentials to clipboard', async () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
const copyButtons = screen.getAllByRole('button');
const adminCopyButton = copyButtons.filter(btn => btn.textContent?.includes('Copy'))[1];
fireEvent.click(adminCopyButton!);
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('admin@example.com\nAdmin123!');
const copiedButtons = screen.getAllByRole('button');
const copiedButton = copiedButtons.find(btn => btn.textContent?.includes('Copied!'));
expect(copiedButton).toBeInTheDocument();
});
});
it('resets copied state after 2 seconds', async () => {
jest.useFakeTimers();
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
const copyButtons = screen.getAllByRole('button');
const copyButton = copyButtons.find(btn => btn.textContent?.includes('Copy'));
fireEvent.click(copyButton!);
await waitFor(() => {
const copiedButtons = screen.getAllByRole('button');
const copiedButton = copiedButtons.find(btn => btn.textContent?.includes('Copied!'));
expect(copiedButton).toBeInTheDocument();
});
jest.advanceTimersByTime(2000);
await waitFor(() => {
const buttons = screen.getAllByRole('button');
const copiedButton = buttons.find(btn => btn.textContent?.includes('Copied!'));
expect(copiedButton).toBeUndefined();
});
jest.useRealTimers();
});
it('handles clipboard copy failure gracefully', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.reject(new Error('Clipboard error'))),
},
});
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
const copyButtons = screen.getAllByRole('button');
const copyButton = copyButtons.find(btn => btn.textContent?.includes('Copy'));
fireEvent.click(copyButton!);
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to copy:', expect.any(Error));
});
consoleErrorSpy.mockRestore();
});
it('calls onClose when close button is clicked', () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
// Find the "Close" button (filter to get the one that's visible and is the footer button)
const closeButtons = screen.getAllByRole('button', { name: 'Close' });
const footerCloseButton = closeButtons.find(btn =>
btn.textContent === 'Close' && !btn.querySelector('.sr-only')
);
fireEvent.click(footerCloseButton!);
expect(mockOnClose).toHaveBeenCalled();
});
it('has a link to login page', () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
const loginLink = screen.getByRole('link', { name: /go to login/i });
expect(loginLink).toHaveAttribute('href', '/login');
});
it('calls onClose when login link is clicked', () => {
render(<DemoCredentialsModal open={true} onClose={mockOnClose} />);
const loginLink = screen.getByRole('link', { name: /go to login/i });
fireEvent.click(loginLink);
expect(mockOnClose).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,152 @@
/**
* Tests for Header component
*/
import { render, screen, fireEvent } from '@testing-library/react';
import { Header } from '@/components/home/Header';
// Mock Next.js Link
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
// Mock DemoCredentialsModal
jest.mock('@/components/home/DemoCredentialsModal', () => ({
DemoCredentialsModal: ({ open, onClose }: any) => (
open ? <div data-testid="demo-modal">
<button onClick={onClose}>Close Modal</button>
</div> : null
),
}));
describe('Header', () => {
it('renders logo', () => {
render(<Header />);
expect(screen.getByText('FastNext')).toBeInTheDocument();
expect(screen.getByText('Template')).toBeInTheDocument();
});
it('logo links to homepage', () => {
render(<Header />);
const logoLink = screen.getByRole('link', { name: /fastnext template/i });
expect(logoLink).toHaveAttribute('href', '/');
});
describe('Desktop Navigation', () => {
it('renders navigation links', () => {
render(<Header />);
expect(screen.getByRole('link', { name: 'Components' })).toHaveAttribute('href', '/dev');
expect(screen.getByRole('link', { name: 'Admin Demo' })).toHaveAttribute('href', '/admin');
});
it('renders GitHub link with star badge', () => {
render(<Header />);
const githubLinks = screen.getAllByRole('link', { name: /github/i });
const desktopGithubLink = githubLinks.find(link =>
link.getAttribute('href')?.includes('github.com')
);
expect(desktopGithubLink).toHaveAttribute('href', 'https://github.com/your-org/fast-next-template');
expect(desktopGithubLink).toHaveAttribute('target', '_blank');
expect(desktopGithubLink).toHaveAttribute('rel', 'noopener noreferrer');
});
it('renders Try Demo button', () => {
render(<Header />);
const demoButton = screen.getByRole('button', { name: /try demo/i });
expect(demoButton).toBeInTheDocument();
});
it('renders Login button', () => {
render(<Header />);
const loginLinks = screen.getAllByRole('link', { name: /login/i });
expect(loginLinks.length).toBeGreaterThan(0);
expect(loginLinks[0]).toHaveAttribute('href', '/login');
});
it('opens demo modal when Try Demo button is clicked', () => {
render(<Header />);
const demoButton = screen.getByRole('button', { name: /try demo/i });
fireEvent.click(demoButton);
expect(screen.getByTestId('demo-modal')).toBeInTheDocument();
});
});
describe('Mobile Menu', () => {
it('renders mobile menu toggle button', () => {
render(<Header />);
// SheetTrigger wraps the button, so we need to find it by aria-label
const menuButton = screen.getByRole('button', { name: /toggle menu/i });
expect(menuButton).toBeInTheDocument();
});
it('mobile menu contains navigation links', () => {
render(<Header />);
// Note: SheetContent is hidden by default in tests, but we can verify the links exist
// The actual mobile menu behavior is tested in E2E tests
const componentsLinks = screen.getAllByRole('link', { name: /components/i });
expect(componentsLinks.length).toBeGreaterThan(0);
});
it('mobile menu contains GitHub link', () => {
render(<Header />);
const githubLinks = screen.getAllByRole('link', { name: /github/i });
expect(githubLinks.length).toBeGreaterThan(0);
});
});
describe('Demo Modal Integration', () => {
it('closes demo modal when close is called', () => {
render(<Header />);
// Open modal
const demoButton = screen.getByRole('button', { name: /try demo/i });
fireEvent.click(demoButton);
expect(screen.getByTestId('demo-modal')).toBeInTheDocument();
// Close modal
const closeButton = screen.getByText('Close Modal');
fireEvent.click(closeButton);
expect(screen.queryByTestId('demo-modal')).not.toBeInTheDocument();
});
});
describe('Accessibility', () => {
it('has proper ARIA labels for icon buttons', () => {
render(<Header />);
const menuButton = screen.getByRole('button', { name: /toggle menu/i });
expect(menuButton).toHaveAccessibleName();
});
it('has proper external link attributes', () => {
render(<Header />);
const githubLinks = screen.getAllByRole('link', { name: /github/i });
const externalLink = githubLinks.find(link =>
link.getAttribute('href')?.includes('github.com')
);
expect(externalLink).toHaveAttribute('target', '_blank');
expect(externalLink).toHaveAttribute('rel', 'noopener noreferrer');
});
});
});

View File

@@ -0,0 +1,137 @@
/**
* Tests for HeroSection component
*/
import { render, screen, fireEvent } from '@testing-library/react';
import { HeroSection } from '@/components/home/HeroSection';
// Mock framer-motion
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
h1: ({ children, ...props }: any) => <h1 {...props}>{children}</h1>,
p: ({ children, ...props }: any) => <p {...props}>{children}</p>,
},
}));
// Mock Next.js Link
jest.mock('next/link', () => ({
__esModule: true,
default: ({ children, href, ...props }: any) => {
return (
<a href={href} {...props}>
{children}
</a>
);
},
}));
// Mock DemoCredentialsModal
jest.mock('@/components/home/DemoCredentialsModal', () => ({
DemoCredentialsModal: ({ open, onClose }: any) => (
open ? <div data-testid="demo-modal">
<button onClick={onClose}>Close Modal</button>
</div> : null
),
}));
describe('HeroSection', () => {
it('renders badge with key highlights', () => {
render(<HeroSection />);
expect(screen.getByText('MIT Licensed')).toBeInTheDocument();
expect(screen.getAllByText('97% Test Coverage')[0]).toBeInTheDocument();
expect(screen.getByText('Production Ready')).toBeInTheDocument();
});
it('renders main headline', () => {
render(<HeroSection />);
expect(screen.getAllByText(/Everything You Need to Build/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/Modern Web Applications/i)[0]).toBeInTheDocument();
});
it('renders subheadline with key messaging', () => {
render(<HeroSection />);
expect(screen.getByText(/Production-ready FastAPI \+ Next.js template/i)).toBeInTheDocument();
expect(screen.getByText(/Start building features on day one/i)).toBeInTheDocument();
});
it('renders Try Live Demo button', () => {
render(<HeroSection />);
const demoButton = screen.getByRole('button', { name: /try live demo/i });
expect(demoButton).toBeInTheDocument();
});
it('renders View on GitHub link', () => {
render(<HeroSection />);
const githubLink = screen.getByRole('link', { name: /view on github/i });
expect(githubLink).toHaveAttribute('href', 'https://github.com/your-org/fast-next-template');
expect(githubLink).toHaveAttribute('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
});
it('renders Explore Components link', () => {
render(<HeroSection />);
const componentsLink = screen.getByRole('link', { name: /explore components/i });
expect(componentsLink).toHaveAttribute('href', '/dev');
});
it('displays test coverage stats', () => {
render(<HeroSection />);
const coverageTexts = screen.getAllByText('97%');
expect(coverageTexts.length).toBeGreaterThan(0);
const testCountTexts = screen.getAllByText('743');
expect(testCountTexts.length).toBeGreaterThan(0);
expect(screen.getAllByText(/Passing Tests/i)[0]).toBeInTheDocument();
expect(screen.getByText('0')).toBeInTheDocument();
expect(screen.getByText(/Flaky Tests/i)).toBeInTheDocument();
});
it('opens demo modal when Try Live Demo button is clicked', () => {
render(<HeroSection />);
const demoButton = screen.getByRole('button', { name: /try live demo/i });
fireEvent.click(demoButton);
expect(screen.getByTestId('demo-modal')).toBeInTheDocument();
});
it('closes demo modal when close is called', () => {
render(<HeroSection />);
// Open modal
const demoButton = screen.getByRole('button', { name: /try live demo/i });
fireEvent.click(demoButton);
expect(screen.getByTestId('demo-modal')).toBeInTheDocument();
// Close modal
const closeButton = screen.getByText('Close Modal');
fireEvent.click(closeButton);
expect(screen.queryByTestId('demo-modal')).not.toBeInTheDocument();
});
describe('Accessibility', () => {
it('has proper heading hierarchy', () => {
render(<HeroSection />);
const heading = screen.getAllByRole('heading', { level: 1 })[0];
expect(heading).toBeInTheDocument();
});
it('has proper external link attributes', () => {
render(<HeroSection />);
const githubLink = screen.getByRole('link', { name: /view on github/i });
expect(githubLink).toHaveAttribute('target', '_blank');
expect(githubLink).toHaveAttribute('rel', 'noopener noreferrer');
});
});
});

View File

@@ -0,0 +1,128 @@
/**
* Tests for QuickStartCode component
*/
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QuickStartCode } from '@/components/home/QuickStartCode';
// Mock framer-motion
jest.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
}));
// Mock react-syntax-highlighter
jest.mock('react-syntax-highlighter', () => ({
Prism: ({ children, ...props }: any) => <pre {...props}>{children}</pre>,
}));
jest.mock('react-syntax-highlighter/dist/esm/styles/prism', () => ({
vscDarkPlus: {},
}));
describe('QuickStartCode', () => {
beforeEach(() => {
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.resolve()),
},
});
});
it('renders the section heading', () => {
render(<QuickStartCode />);
expect(screen.getByText('5-Minute Setup')).toBeInTheDocument();
expect(screen.getByText(/Clone, run, and start building/i)).toBeInTheDocument();
});
it('renders bash indicator', () => {
render(<QuickStartCode />);
expect(screen.getByText('bash')).toBeInTheDocument();
});
it('renders copy button', () => {
render(<QuickStartCode />);
const copyButton = screen.getByRole('button', { name: /copy/i });
expect(copyButton).toBeInTheDocument();
});
it('displays the code snippet', () => {
render(<QuickStartCode />);
const codeBlock = screen.getByText(/git clone/i);
expect(codeBlock).toBeInTheDocument();
});
it('copies code to clipboard when copy button is clicked', async () => {
render(<QuickStartCode />);
const copyButton = screen.getByRole('button', { name: /copy/i });
fireEvent.click(copyButton);
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalled();
});
const clipboardContent = (navigator.clipboard.writeText as jest.Mock).mock.calls[0][0];
expect(clipboardContent).toContain('git clone');
expect(clipboardContent).toContain('docker-compose up');
expect(clipboardContent).toContain('pip install -r requirements.txt');
});
it('shows "Copied!" message after copying', async () => {
render(<QuickStartCode />);
const copyButton = screen.getByRole('button', { name: /copy/i });
fireEvent.click(copyButton);
await waitFor(() => {
expect(screen.getByText('Copied!')).toBeInTheDocument();
});
});
it('resets copied state after 2 seconds', async () => {
jest.useFakeTimers();
render(<QuickStartCode />);
const copyButton = screen.getByRole('button', { name: /copy/i });
fireEvent.click(copyButton);
await waitFor(() => {
expect(screen.getByText('Copied!')).toBeInTheDocument();
});
jest.advanceTimersByTime(2000);
await waitFor(() => {
expect(screen.queryByText('Copied!')).not.toBeInTheDocument();
expect(screen.getByText('Copy')).toBeInTheDocument();
});
jest.useRealTimers();
});
it('handles clipboard copy failure gracefully', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(() => Promise.reject(new Error('Clipboard error'))),
},
});
render(<QuickStartCode />);
const copyButton = screen.getByRole('button', { name: /copy/i });
fireEvent.click(copyButton);
await waitFor(() => {
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to copy:', expect.any(Error));
});
consoleErrorSpy.mockRestore();
});
});

View File

@@ -0,0 +1,115 @@
/**
* Tests for StatsSection component
*/
import { render, screen } from '@testing-library/react';
import { StatsSection } from '@/components/home/StatsSection';
// Mock framer-motion
jest.mock('framer-motion', () => {
const React = require('react');
return {
motion: {
div: ({ children, ...props }: any) => <div {...props}>{children}</div>,
},
useInView: () => true, // Always in view for tests
};
});
describe('StatsSection', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('renders section heading', () => {
render(<StatsSection />);
expect(screen.getByText('Built with Quality in Mind')).toBeInTheDocument();
expect(screen.getByText(/Not just another template/i)).toBeInTheDocument();
});
it('renders all stat cards', () => {
render(<StatsSection />);
expect(screen.getByText('Test Coverage')).toBeInTheDocument();
expect(screen.getByText('Passing Tests')).toBeInTheDocument();
expect(screen.getByText('Flaky Tests')).toBeInTheDocument();
expect(screen.getByText('API Endpoints')).toBeInTheDocument();
});
it('displays stat descriptions', () => {
render(<StatsSection />);
expect(screen.getByText(/Comprehensive testing across backend and frontend/i)).toBeInTheDocument();
expect(screen.getByText(/Backend, frontend unit, and E2E tests/i)).toBeInTheDocument();
expect(screen.getByText(/Production-stable test suite/i)).toBeInTheDocument();
expect(screen.getByText(/Fully documented with OpenAPI/i)).toBeInTheDocument();
});
it('renders animated counters with correct suffixes', () => {
render(<StatsSection />);
// Counters start at 0, so we should see 0 initially
const counters = screen.getAllByText(/^[0-9]+[%+]?$/);
expect(counters.length).toBeGreaterThan(0);
});
it('animates counters when in view', () => {
render(<StatsSection />);
// The useInView mock returns true, so animation should start
// Advance timers to let the counter animation run
jest.advanceTimersByTime(2000);
// After animation, we should see the final values
// The component should eventually show the stat values
const statsSection = screen.getByText('Test Coverage').parentElement;
expect(statsSection).toBeInTheDocument();
});
it('displays icons for each stat', () => {
render(<StatsSection />);
// Icons are rendered via lucide-react components
// We can verify the stat cards are rendered with proper structure
const testCoverageCard = screen.getByText('Test Coverage').closest('div');
expect(testCoverageCard).toBeInTheDocument();
const passingTestsCard = screen.getByText('Passing Tests').closest('div');
expect(passingTestsCard).toBeInTheDocument();
const flakyTestsCard = screen.getByText('Flaky Tests').closest('div');
expect(flakyTestsCard).toBeInTheDocument();
const apiEndpointsCard = screen.getByText('API Endpoints').closest('div');
expect(apiEndpointsCard).toBeInTheDocument();
});
describe('Accessibility', () => {
it('has proper heading hierarchy', () => {
render(<StatsSection />);
const heading = screen.getByRole('heading', { name: /built with quality in mind/i });
expect(heading).toBeInTheDocument();
});
it('has descriptive labels for stats', () => {
render(<StatsSection />);
const statLabels = [
'Test Coverage',
'Passing Tests',
'Flaky Tests',
'API Endpoints',
];
statLabels.forEach(label => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
});
});

View File

@@ -0,0 +1,178 @@
/**
* Tests for usePrefersReducedMotion hook
* Tests media query detection for accessibility preferences
*/
import { renderHook, act } from '@testing-library/react';
import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion';
describe('usePrefersReducedMotion', () => {
let mockMatchMedia: jest.Mock;
let mockListeners: ((event: MediaQueryListEvent) => void)[];
beforeEach(() => {
mockListeners = [];
mockMatchMedia = jest.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
if (event === 'change') {
mockListeners.push(listener);
}
}),
removeEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
if (event === 'change') {
const index = mockListeners.indexOf(listener);
if (index > -1) {
mockListeners.splice(index, 1);
}
}
}),
dispatchEvent: jest.fn(),
}));
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: mockMatchMedia,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('returns false when user does not prefer reduced motion', () => {
mockMatchMedia.mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
const { result } = renderHook(() => usePrefersReducedMotion());
expect(result.current).toBe(false);
expect(mockMatchMedia).toHaveBeenCalledWith('(prefers-reduced-motion: reduce)');
});
it('returns true when user prefers reduced motion', () => {
mockMatchMedia.mockImplementation((query: string) => ({
matches: true,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
const { result } = renderHook(() => usePrefersReducedMotion());
expect(result.current).toBe(true);
});
it('updates when media query preference changes to true', () => {
const mockMediaQuery = {
matches: false,
media: '(prefers-reduced-motion: reduce)',
onchange: null,
addEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
if (event === 'change') {
mockListeners.push(listener);
}
}),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
};
mockMatchMedia.mockReturnValue(mockMediaQuery);
const { result } = renderHook(() => usePrefersReducedMotion());
expect(result.current).toBe(false);
// Simulate media query change
act(() => {
mockListeners.forEach(listener => {
listener({ matches: true } as MediaQueryListEvent);
});
});
expect(result.current).toBe(true);
});
it('updates when media query preference changes to false', () => {
const mockMediaQuery = {
matches: true,
media: '(prefers-reduced-motion: reduce)',
onchange: null,
addEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
if (event === 'change') {
mockListeners.push(listener);
}
}),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
};
mockMatchMedia.mockReturnValue(mockMediaQuery);
const { result } = renderHook(() => usePrefersReducedMotion());
expect(result.current).toBe(true);
// Simulate media query change
act(() => {
mockListeners.forEach(listener => {
listener({ matches: false } as MediaQueryListEvent);
});
});
expect(result.current).toBe(false);
});
it('cleans up event listener on unmount', () => {
const removeEventListenerSpy = jest.fn();
const mockMediaQuery = {
matches: false,
media: '(prefers-reduced-motion: reduce)',
onchange: null,
addEventListener: jest.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
if (event === 'change') {
mockListeners.push(listener);
}
}),
removeEventListener: removeEventListenerSpy,
dispatchEvent: jest.fn(),
};
mockMatchMedia.mockReturnValue(mockMediaQuery);
const { unmount } = renderHook(() => usePrefersReducedMotion());
expect(mockMediaQuery.addEventListener).toHaveBeenCalledWith('change', expect.any(Function));
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith('change', expect.any(Function));
});
it('handles SSR environment safely', () => {
const originalWindow = global.window;
// @ts-ignore - Simulating SSR
delete global.window;
const { result } = renderHook(() => usePrefersReducedMotion());
// Should return false in SSR environment
expect(result.current).toBe(false);
// Restore window
global.window = originalWindow;
});
});

View File

@@ -0,0 +1,117 @@
/**
* Tests for chart-colors utility
* Tests color configuration and helper functions for data visualization
*/
import { withOpacity, CHART_COLORS, CHART_PALETTES, CHART_GRADIENTS } from '@/lib/chart-colors';
describe('chart-colors', () => {
describe('withOpacity', () => {
it('converts opacity 0 to hex 00', () => {
const result = withOpacity('#3b82f6', 0);
expect(result).toBe('#3b82f600');
});
it('converts opacity 1 to hex ff', () => {
const result = withOpacity('#3b82f6', 1);
expect(result).toBe('#3b82f6ff');
});
it('converts opacity 0.5 to hex 80', () => {
const result = withOpacity('#3b82f6', 0.5);
expect(result).toBe('#3b82f680');
});
it('converts opacity 0.25 to hex 40', () => {
const result = withOpacity('#3b82f6', 0.25);
expect(result).toBe('#3b82f640');
});
it('converts opacity 0.75 to hex bf', () => {
const result = withOpacity('#3b82f6', 0.75);
expect(result).toBe('#3b82f6bf');
});
it('pads single digit hex values with zero', () => {
const result = withOpacity('#3b82f6', 0.01);
expect(result).toBe('#3b82f603');
});
it('works with 3-digit hex colors', () => {
const result = withOpacity('#fff', 0.5);
expect(result).toBe('#fff80');
});
it('works with uppercase hex colors', () => {
const result = withOpacity('#3B82F6', 0.5);
expect(result).toBe('#3B82F680');
});
it('handles edge case of very small opacity', () => {
const result = withOpacity('#000000', 0.004);
expect(result).toBe('#00000001');
});
it('handles edge case of very high opacity', () => {
const result = withOpacity('#ffffff', 0.996);
expect(result).toBe('#fffffffe');
});
});
describe('CHART_COLORS', () => {
it('exports primary color palette', () => {
expect(CHART_COLORS.primary).toBe('#3b82f6');
expect(CHART_COLORS.primaryLight).toBe('#60a5fa');
expect(CHART_COLORS.primaryDark).toBe('#2563eb');
});
it('exports accent colors', () => {
expect(CHART_COLORS.accent1).toBeDefined();
expect(CHART_COLORS.accent2).toBeDefined();
expect(CHART_COLORS.accent3).toBeDefined();
expect(CHART_COLORS.accent4).toBeDefined();
expect(CHART_COLORS.accent5).toBeDefined();
});
it('exports status colors', () => {
expect(CHART_COLORS.success).toBeDefined();
expect(CHART_COLORS.warning).toBeDefined();
expect(CHART_COLORS.error).toBeDefined();
expect(CHART_COLORS.info).toBeDefined();
});
});
describe('CHART_PALETTES', () => {
it('exports line chart palette', () => {
expect(CHART_PALETTES.line).toHaveLength(2);
expect(CHART_PALETTES.line).toContain(CHART_COLORS.primary);
expect(CHART_PALETTES.line).toContain(CHART_COLORS.accent1);
});
it('exports bar chart palette', () => {
expect(CHART_PALETTES.bar).toHaveLength(2);
expect(CHART_PALETTES.bar).toContain(CHART_COLORS.primary);
expect(CHART_PALETTES.bar).toContain(CHART_COLORS.accent2);
});
it('exports pie chart palette with 4 colors', () => {
expect(CHART_PALETTES.pie).toHaveLength(4);
});
it('exports multi-series palette with 6 colors', () => {
expect(CHART_PALETTES.multi).toHaveLength(6);
});
});
describe('CHART_GRADIENTS', () => {
it('exports primary gradient definition', () => {
expect(CHART_GRADIENTS.primary.start).toBe(CHART_COLORS.primary);
expect(CHART_GRADIENTS.primary.end).toMatch(/#3b82f6[a-f0-9]{2}/);
});
it('exports accent gradient definition', () => {
expect(CHART_GRADIENTS.accent.start).toBe(CHART_COLORS.accent1);
expect(CHART_GRADIENTS.accent.end).toMatch(/#8b5cf6[a-f0-9]{2}/);
});
});
});