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:
@@ -16,6 +16,9 @@ const customJestConfig = {
|
||||
'<rootDir>/tests/**/*.test.ts',
|
||||
'<rootDir>/tests/**/*.test.tsx',
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(react-syntax-highlighter|refractor|hastscript|hast-.*|unist-.*|property-information|space-separated-tokens|comma-separated-tokens|web-namespaces)/)',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{js,jsx,ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
|
||||
@@ -23,6 +23,19 @@ global.BroadcastChannel = class BroadcastChannel {
|
||||
removeEventListener() {}
|
||||
};
|
||||
|
||||
// Mock IntersectionObserver for components that use viewport detection
|
||||
global.IntersectionObserver = class IntersectionObserver {
|
||||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
observe() {
|
||||
// Immediately trigger the callback with isIntersecting: true for tests
|
||||
this.callback([{ isIntersecting: true }]);
|
||||
}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
// Use real Web Crypto API polyfill for Node environment
|
||||
const cryptoPolyfill = new Crypto();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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', () => {
|
||||
describe('Page Structure', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<Home />);
|
||||
expect(screen.getByText(/get started by editing/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('banner')).toBeInTheDocument(); // header
|
||||
expect(screen.getByRole('main')).toBeInTheDocument();
|
||||
expect(screen.getByRole('contentinfo')).toBeInTheDocument(); // footer
|
||||
});
|
||||
|
||||
it('renders Next.js logo', () => {
|
||||
it('renders header with logo', () => {
|
||||
render(<Home />);
|
||||
|
||||
const logo = screen.getByAltText('Next.js logo');
|
||||
expect(logo).toBeInTheDocument();
|
||||
expect(logo).toHaveAttribute('src', '/next.svg');
|
||||
const header = screen.getByRole('banner');
|
||||
expect(within(header).getByText('FastNext')).toBeInTheDocument();
|
||||
expect(within(header).getByText('Template')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Vercel logo', () => {
|
||||
it('renders footer with copyright', () => {
|
||||
render(<Home />);
|
||||
|
||||
const logo = screen.getByAltText('Vercel logomark');
|
||||
expect(logo).toBeInTheDocument();
|
||||
expect(logo).toHaveAttribute('src', '/vercel.svg');
|
||||
const footer = screen.getByRole('contentinfo');
|
||||
expect(within(footer).getByText(/FastNext Template. MIT Licensed/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('has correct external links', () => {
|
||||
describe('Hero Section', () => {
|
||||
it('renders main headline', () => {
|
||||
render(<Home />);
|
||||
|
||||
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');
|
||||
|
||||
const docsLink = screen.getByRole('link', { name: /read our docs/i });
|
||||
expect(docsLink).toHaveAttribute('href', expect.stringContaining('nextjs.org/docs'));
|
||||
expect(docsLink).toHaveAttribute('target', '_blank');
|
||||
expect(screen.getAllByText(/Everything You Need to Build/i)[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/Modern Web Applications/i)[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders footer links', () => {
|
||||
it('renders production-ready messaging', () => {
|
||||
render(<Home />);
|
||||
|
||||
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();
|
||||
expect(screen.getByText(/Production-ready FastAPI/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('has accessible image alt texts', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
describe('Context Section', () => {
|
||||
it('renders what you get message', () => {
|
||||
render(<Home />);
|
||||
expect(screen.getByText(/What You Get Out of the Box/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature Grid', () => {
|
||||
it('renders comprehensive features heading', () => {
|
||||
render(<Home />);
|
||||
expect(screen.getByText(/Comprehensive Features, No Assembly Required/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Demo Section', () => {
|
||||
it('renders demo section heading', () => {
|
||||
render(<Home />);
|
||||
expect(screen.getByText(/See It In Action/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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tech Stack Section', () => {
|
||||
it('renders tech stack heading', () => {
|
||||
render(<Home />);
|
||||
expect(screen.getByText(/Modern, Type-Safe, Production-Grade Stack/i)).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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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%');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
99
frontend/tests/components/home/AnimatedTerminal.test.tsx
Normal file
99
frontend/tests/components/home/AnimatedTerminal.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
132
frontend/tests/components/home/CTASection.test.tsx
Normal file
132
frontend/tests/components/home/CTASection.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
170
frontend/tests/components/home/DemoCredentialsModal.test.tsx
Normal file
170
frontend/tests/components/home/DemoCredentialsModal.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
152
frontend/tests/components/home/Header.test.tsx
Normal file
152
frontend/tests/components/home/Header.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
137
frontend/tests/components/home/HeroSection.test.tsx
Normal file
137
frontend/tests/components/home/HeroSection.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
128
frontend/tests/components/home/QuickStartCode.test.tsx
Normal file
128
frontend/tests/components/home/QuickStartCode.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
115
frontend/tests/components/home/StatsSection.test.tsx
Normal file
115
frontend/tests/components/home/StatsSection.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
178
frontend/tests/hooks/usePrefersReducedMotion.test.ts
Normal file
178
frontend/tests/hooks/usePrefersReducedMotion.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
117
frontend/tests/lib/chart-colors.test.ts
Normal file
117
frontend/tests/lib/chart-colors.test.ts
Normal 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}/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user