diff --git a/frontend/src/components/dev/ComponentShowcase.tsx b/frontend/src/components/dev/ComponentShowcase.tsx
index b94d438..c56bcdf 100644
--- a/frontend/src/components/dev/ComponentShowcase.tsx
+++ b/frontend/src/components/dev/ComponentShowcase.tsx
@@ -1,6 +1,9 @@
+/* istanbul ignore file */
+
/**
* Component Showcase
* Comprehensive display of all design system components
+ * This file is excluded from coverage as it's a demo/showcase page
*/
'use client';
diff --git a/frontend/tests/components/auth/AuthInitializer.test.tsx b/frontend/tests/components/auth/AuthInitializer.test.tsx
new file mode 100644
index 0000000..9b7a0a7
--- /dev/null
+++ b/frontend/tests/components/auth/AuthInitializer.test.tsx
@@ -0,0 +1,58 @@
+/**
+ * Tests for AuthInitializer
+ * Verifies authentication state is loaded from storage on mount
+ */
+
+import { render, waitFor } from '@testing-library/react';
+import { AuthInitializer } from '@/components/auth/AuthInitializer';
+import { useAuthStore } from '@/stores/authStore';
+
+// Mock the auth store
+jest.mock('@/stores/authStore', () => ({
+ useAuthStore: jest.fn(),
+}));
+
+describe('AuthInitializer', () => {
+ const mockLoadAuthFromStorage = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ (useAuthStore as unknown as jest.Mock).mockImplementation((selector: any) => {
+ const state = {
+ loadAuthFromStorage: mockLoadAuthFromStorage,
+ };
+ return selector(state);
+ });
+ });
+
+ describe('Initialization', () => {
+ it('renders nothing (null)', () => {
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it('calls loadAuthFromStorage on mount', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(mockLoadAuthFromStorage).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it('does not call loadAuthFromStorage again on re-render', async () => {
+ const { rerender } = render();
+
+ await waitFor(() => {
+ expect(mockLoadAuthFromStorage).toHaveBeenCalledTimes(1);
+ });
+
+ // Force re-render
+ rerender();
+
+ // Should still only be called once (useEffect dependencies prevent re-call)
+ expect(mockLoadAuthFromStorage).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/frontend/tests/components/layout/Footer.test.tsx b/frontend/tests/components/layout/Footer.test.tsx
new file mode 100644
index 0000000..8d14eab
--- /dev/null
+++ b/frontend/tests/components/layout/Footer.test.tsx
@@ -0,0 +1,33 @@
+/**
+ * Tests for Footer Component
+ * Verifies footer rendering and content
+ */
+
+import { render, screen } from '@testing-library/react';
+import { Footer } from '@/components/layout/Footer';
+
+describe('Footer', () => {
+ describe('Rendering', () => {
+ it('renders footer element', () => {
+ const { container } = render();
+
+ const footer = container.querySelector('footer');
+ expect(footer).toBeInTheDocument();
+ });
+
+ it('displays copyright text with current year', () => {
+ render();
+
+ const currentYear = new Date().getFullYear();
+ expect(screen.getByText(`© ${currentYear} FastNext Template. All rights reserved.`)).toBeInTheDocument();
+ });
+
+ it('applies correct styling classes', () => {
+ const { container } = render();
+
+ const footer = container.querySelector('footer');
+ expect(footer).toHaveClass('border-t');
+ expect(footer).toHaveClass('bg-muted/30');
+ });
+ });
+});
diff --git a/frontend/tests/components/layout/Header.test.tsx b/frontend/tests/components/layout/Header.test.tsx
new file mode 100644
index 0000000..25c9af2
--- /dev/null
+++ b/frontend/tests/components/layout/Header.test.tsx
@@ -0,0 +1,345 @@
+/**
+ * Tests for Header Component
+ * Verifies navigation, user menu, and auth-based rendering
+ */
+
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { Header } from '@/components/layout/Header';
+import { useAuthStore } from '@/stores/authStore';
+import { useLogout } from '@/lib/api/hooks/useAuth';
+import { usePathname } from 'next/navigation';
+import type { User } from '@/stores/authStore';
+
+// Mock dependencies
+jest.mock('@/stores/authStore', () => ({
+ useAuthStore: jest.fn(),
+}));
+
+jest.mock('@/lib/api/hooks/useAuth', () => ({
+ useLogout: jest.fn(),
+}));
+
+jest.mock('next/navigation', () => ({
+ usePathname: jest.fn(),
+}));
+
+jest.mock('@/components/theme', () => ({
+ ThemeToggle: () =>
Theme Toggle
,
+}));
+
+// Helper to create mock user
+function createMockUser(overrides: Partial = {}): User {
+ return {
+ id: 'user-123',
+ email: 'test@example.com',
+ first_name: 'Test',
+ last_name: 'User',
+ phone_number: null,
+ is_active: true,
+ is_superuser: false,
+ created_at: new Date().toISOString(),
+ updated_at: null,
+ ...overrides,
+ };
+}
+
+describe('Header', () => {
+ const mockLogout = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ (usePathname as jest.Mock).mockReturnValue('/');
+
+ (useLogout as jest.Mock).mockReturnValue({
+ mutate: mockLogout,
+ isPending: false,
+ });
+ });
+
+ describe('Rendering', () => {
+ it('renders header with logo', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ expect(screen.getByText('FastNext')).toBeInTheDocument();
+ });
+
+ it('renders theme toggle', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ expect(screen.getByTestId('theme-toggle')).toBeInTheDocument();
+ });
+
+ it('renders user avatar with initials', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({
+ first_name: 'John',
+ last_name: 'Doe',
+ }),
+ });
+
+ render();
+
+ expect(screen.getByText('JD')).toBeInTheDocument();
+ });
+
+ it('renders user avatar with single initial when no last name', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({
+ first_name: 'John',
+ last_name: null,
+ }),
+ });
+
+ render();
+
+ expect(screen.getByText('J')).toBeInTheDocument();
+ });
+
+ it('renders default initial when no first name', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({
+ first_name: '',
+ }),
+ });
+
+ render();
+
+ expect(screen.getByText('U')).toBeInTheDocument();
+ });
+ });
+
+ describe('Navigation Links', () => {
+ it('renders home link', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ const homeLink = screen.getByRole('link', { name: /home/i });
+ expect(homeLink).toHaveAttribute('href', '/');
+ });
+
+ it('renders admin link for superusers', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({ is_superuser: true }),
+ });
+
+ render();
+
+ const adminLink = screen.getByRole('link', { name: /admin/i });
+ expect(adminLink).toHaveAttribute('href', '/admin');
+ });
+
+ it('does not render admin link for regular users', () => {
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({ is_superuser: false }),
+ });
+
+ render();
+
+ const adminLinks = screen.queryAllByRole('link', { name: /admin/i });
+ // Filter out the one in the dropdown menu
+ const navAdminLinks = adminLinks.filter(
+ (link) => !link.closest('[role="menu"]')
+ );
+ expect(navAdminLinks).toHaveLength(0);
+ });
+
+ it('highlights active navigation link', () => {
+ (usePathname as jest.Mock).mockReturnValue('/admin');
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({ is_superuser: true }),
+ });
+
+ render();
+
+ const adminLink = screen.getByRole('link', { name: /admin/i });
+ expect(adminLink).toHaveClass('bg-primary');
+ });
+ });
+
+ describe('User Dropdown Menu', () => {
+ it('opens dropdown when avatar is clicked', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({
+ first_name: 'John',
+ last_name: 'Doe',
+ email: 'john@example.com',
+ }),
+ });
+
+ render();
+
+ // Find avatar button by looking for the button containing the avatar initials
+ const avatarButton = screen.getByText('JD').closest('button')!;
+ await user.click(avatarButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
+ });
+ });
+
+ it('displays user info in dropdown', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({
+ first_name: 'John',
+ last_name: 'Doe',
+ email: 'john@example.com',
+ }),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('JD').closest('button')!;
+ await user.click(avatarButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('John Doe')).toBeInTheDocument();
+ expect(screen.getByText('john@example.com')).toBeInTheDocument();
+ });
+ });
+
+ it('includes profile link in dropdown', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ const profileLink = await screen.findByRole('menuitem', { name: /profile/i });
+ expect(profileLink).toHaveAttribute('href', '/settings/profile');
+ });
+
+ it('includes settings link in dropdown', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ const settingsLink = await screen.findByRole('menuitem', { name: /settings/i });
+ expect(settingsLink).toHaveAttribute('href', '/settings/password');
+ });
+
+ it('includes admin panel link for superusers', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({ is_superuser: true }),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ const adminLink = await screen.findByRole('menuitem', { name: /admin panel/i });
+ expect(adminLink).toHaveAttribute('href', '/admin');
+ });
+
+ it('does not include admin panel link for regular users', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser({ is_superuser: false }),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ await waitFor(() => {
+ expect(screen.queryByRole('menuitem', { name: /admin panel/i })).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Logout Functionality', () => {
+ it('calls logout when logout button is clicked', async () => {
+ const user = userEvent.setup();
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ const logoutButton = await screen.findByRole('menuitem', { name: /log out/i });
+ await user.click(logoutButton);
+
+ expect(mockLogout).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows loading state when logging out', async () => {
+ const user = userEvent.setup();
+
+ (useLogout as jest.Mock).mockReturnValue({
+ mutate: mockLogout,
+ isPending: true,
+ });
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ await waitFor(() => {
+ expect(screen.getByText('Logging out...')).toBeInTheDocument();
+ });
+ });
+
+ it('disables logout button when logging out', async () => {
+ const user = userEvent.setup();
+
+ (useLogout as jest.Mock).mockReturnValue({
+ mutate: mockLogout,
+ isPending: true,
+ });
+
+ (useAuthStore as unknown as jest.Mock).mockReturnValue({
+ user: createMockUser(),
+ });
+
+ render();
+
+ const avatarButton = screen.getByText('TU').closest('button')!;
+ await user.click(avatarButton);
+
+ const logoutButton = await screen.findByRole('menuitem', { name: /logging out/i });
+ expect(logoutButton).toHaveAttribute('data-disabled');
+ });
+ });
+});
diff --git a/frontend/tests/components/theme/ThemeProvider.test.tsx b/frontend/tests/components/theme/ThemeProvider.test.tsx
new file mode 100644
index 0000000..3464dd3
--- /dev/null
+++ b/frontend/tests/components/theme/ThemeProvider.test.tsx
@@ -0,0 +1,349 @@
+/**
+ * Tests for ThemeProvider
+ * Verifies theme state management, localStorage persistence, and system preference detection
+ */
+
+import { render, screen, waitFor } from '@testing-library/react';
+import { act } from 'react';
+import { ThemeProvider, useTheme } from '@/components/theme/ThemeProvider';
+
+// Test component to access theme context
+function TestComponent() {
+ const { theme, setTheme, resolvedTheme } = useTheme();
+
+ return (
+
+
{theme}
+
{resolvedTheme}
+
+
+
+
+ );
+}
+
+describe('ThemeProvider', () => {
+ let mockLocalStorage: { [key: string]: string };
+ let mockMatchMedia: jest.Mock;
+
+ beforeEach(() => {
+ // Mock localStorage
+ mockLocalStorage = {};
+
+ Object.defineProperty(window, 'localStorage', {
+ value: {
+ getItem: jest.fn((key: string) => mockLocalStorage[key] || null),
+ setItem: jest.fn((key: string, value: string) => {
+ mockLocalStorage[key] = value;
+ }),
+ removeItem: jest.fn((key: string) => {
+ delete mockLocalStorage[key];
+ }),
+ clear: jest.fn(() => {
+ mockLocalStorage = {};
+ }),
+ },
+ writable: true,
+ });
+
+ // Mock matchMedia
+ mockMatchMedia = jest.fn().mockImplementation((query: string) => ({
+ matches: query === '(prefers-color-scheme: dark)' ? false : false,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ }));
+
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: mockMatchMedia,
+ });
+
+ // Mock document.documentElement
+ document.documentElement.classList.remove('light', 'dark');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Initialization', () => {
+ it('defaults to system theme when no stored preference', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('current-theme')).toHaveTextContent('system');
+ });
+
+ it('loads stored theme preference from localStorage', async () => {
+ mockLocalStorage['theme'] = 'dark';
+
+ render(
+
+
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('current-theme')).toHaveTextContent('dark');
+ });
+ });
+
+ it('ignores invalid theme values from localStorage', () => {
+ mockLocalStorage['theme'] = 'invalid';
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('current-theme')).toHaveTextContent('system');
+ });
+ });
+
+ describe('Theme Switching', () => {
+ it('updates theme when setTheme is called', async () => {
+ render(
+
+
+
+ );
+
+ const lightButton = screen.getByRole('button', { name: 'Set Light' });
+
+ await act(async () => {
+ lightButton.click();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('current-theme')).toHaveTextContent('light');
+ });
+ });
+
+ it('persists theme to localStorage when changed', async () => {
+ render(
+
+
+
+ );
+
+ const darkButton = screen.getByRole('button', { name: 'Set Dark' });
+
+ await act(async () => {
+ darkButton.click();
+ });
+
+ await waitFor(() => {
+ expect(localStorage.setItem).toHaveBeenCalledWith('theme', 'dark');
+ });
+ });
+ });
+
+ describe('Resolved Theme', () => {
+ it('resolves light theme correctly', async () => {
+ render(
+
+
+
+ );
+
+ const lightButton = screen.getByRole('button', { name: 'Set Light' });
+
+ await act(async () => {
+ lightButton.click();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('resolved-theme')).toHaveTextContent('light');
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+ expect(document.documentElement.classList.contains('dark')).toBe(false);
+ });
+ });
+
+ it('resolves dark theme correctly', async () => {
+ render(
+
+
+
+ );
+
+ const darkButton = screen.getByRole('button', { name: 'Set Dark' });
+
+ await act(async () => {
+ darkButton.click();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('resolved-theme')).toHaveTextContent('dark');
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
+ expect(document.documentElement.classList.contains('light')).toBe(false);
+ });
+ });
+
+ it('resolves system theme to light when system prefers light', async () => {
+ mockMatchMedia.mockImplementation((query: string) => ({
+ matches: query === '(prefers-color-scheme: dark)' ? false : false,
+ media: query,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ }));
+
+ render(
+
+
+
+ );
+
+ const systemButton = screen.getByRole('button', { name: 'Set System' });
+
+ await act(async () => {
+ systemButton.click();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('current-theme')).toHaveTextContent('system');
+ expect(screen.getByTestId('resolved-theme')).toHaveTextContent('light');
+ });
+ });
+
+ it('resolves system theme to dark when system prefers dark', async () => {
+ mockMatchMedia.mockImplementation((query: string) => ({
+ matches: query === '(prefers-color-scheme: dark)' ? true : false,
+ media: query,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ }));
+
+ render(
+
+
+
+ );
+
+ const systemButton = screen.getByRole('button', { name: 'Set System' });
+
+ await act(async () => {
+ systemButton.click();
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('current-theme')).toHaveTextContent('system');
+ expect(screen.getByTestId('resolved-theme')).toHaveTextContent('dark');
+ });
+ });
+ });
+
+ describe('DOM Updates', () => {
+ it('applies theme class to document element', async () => {
+ render(
+
+
+
+ );
+
+ const lightButton = screen.getByRole('button', { name: 'Set Light' });
+
+ await act(async () => {
+ lightButton.click();
+ });
+
+ await waitFor(() => {
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+ });
+ });
+
+ it('removes previous theme class when switching', async () => {
+ render(
+
+
+
+ );
+
+ // Set to light
+ await act(async () => {
+ screen.getByRole('button', { name: 'Set Light' }).click();
+ });
+
+ await waitFor(() => {
+ expect(document.documentElement.classList.contains('light')).toBe(true);
+ });
+
+ // Switch to dark
+ await act(async () => {
+ screen.getByRole('button', { name: 'Set Dark' }).click();
+ });
+
+ await waitFor(() => {
+ expect(document.documentElement.classList.contains('dark')).toBe(true);
+ expect(document.documentElement.classList.contains('light')).toBe(false);
+ });
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('throws error when useTheme is used outside provider', () => {
+ // Suppress console.error for this test
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ expect(() => {
+ render();
+ }).toThrow('useTheme must be used within ThemeProvider');
+
+ consoleError.mockRestore();
+ });
+ });
+
+ describe('System Preference Changes', () => {
+ it('listens to system preference changes', () => {
+ const mockAddEventListener = jest.fn();
+
+ mockMatchMedia.mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ addEventListener: mockAddEventListener,
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ }));
+
+ render(
+
+
+
+ );
+
+ expect(mockAddEventListener).toHaveBeenCalledWith('change', expect.any(Function));
+ });
+
+ it('cleans up event listener on unmount', () => {
+ const mockRemoveEventListener = jest.fn();
+
+ mockMatchMedia.mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ addEventListener: jest.fn(),
+ removeEventListener: mockRemoveEventListener,
+ dispatchEvent: jest.fn(),
+ }));
+
+ const { unmount } = render(
+
+
+
+ );
+
+ unmount();
+
+ expect(mockRemoveEventListener).toHaveBeenCalledWith('change', expect.any(Function));
+ });
+ });
+});
diff --git a/frontend/tests/components/theme/ThemeToggle.test.tsx b/frontend/tests/components/theme/ThemeToggle.test.tsx
new file mode 100644
index 0000000..e63d9db
--- /dev/null
+++ b/frontend/tests/components/theme/ThemeToggle.test.tsx
@@ -0,0 +1,186 @@
+/**
+ * Tests for ThemeToggle
+ * Verifies theme toggle button functionality and dropdown menu
+ */
+
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ThemeToggle } from '@/components/theme/ThemeToggle';
+import { ThemeProvider, useTheme } from '@/components/theme/ThemeProvider';
+
+// Mock theme provider for controlled testing
+jest.mock('@/components/theme/ThemeProvider', () => {
+ const actual = jest.requireActual('@/components/theme/ThemeProvider');
+ return {
+ ...actual,
+ useTheme: jest.fn(),
+ };
+});
+
+describe('ThemeToggle', () => {
+ const mockSetTheme = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ // Default mock return value
+ (useTheme as jest.Mock).mockReturnValue({
+ theme: 'system',
+ setTheme: mockSetTheme,
+ resolvedTheme: 'light',
+ });
+ });
+
+ describe('Rendering', () => {
+ it('renders theme toggle button', () => {
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ expect(button).toBeInTheDocument();
+ });
+
+ it('displays sun icon when resolved theme is light', () => {
+ (useTheme as jest.Mock).mockReturnValue({
+ theme: 'light',
+ setTheme: mockSetTheme,
+ resolvedTheme: 'light',
+ });
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ // Sun icon should be visible
+ expect(button.querySelector('svg')).toBeInTheDocument();
+ });
+
+ it('displays moon icon when resolved theme is dark', () => {
+ (useTheme as jest.Mock).mockReturnValue({
+ theme: 'dark',
+ setTheme: mockSetTheme,
+ resolvedTheme: 'dark',
+ });
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ // Moon icon should be visible
+ expect(button.querySelector('svg')).toBeInTheDocument();
+ });
+ });
+
+ describe('Dropdown Menu', () => {
+ it('opens dropdown menu when button is clicked', async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ await waitFor(() => {
+ expect(screen.getByRole('menuitem', { name: /light/i })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: /dark/i })).toBeInTheDocument();
+ expect(screen.getByRole('menuitem', { name: /system/i })).toBeInTheDocument();
+ });
+ });
+
+ it('calls setTheme with "light" when light option is clicked', async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ const lightOption = await screen.findByRole('menuitem', { name: /light/i });
+ await user.click(lightOption);
+
+ expect(mockSetTheme).toHaveBeenCalledWith('light');
+ });
+
+ it('calls setTheme with "dark" when dark option is clicked', async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ const darkOption = await screen.findByRole('menuitem', { name: /dark/i });
+ await user.click(darkOption);
+
+ expect(mockSetTheme).toHaveBeenCalledWith('dark');
+ });
+
+ it('calls setTheme with "system" when system option is clicked', async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ const systemOption = await screen.findByRole('menuitem', { name: /system/i });
+ await user.click(systemOption);
+
+ expect(mockSetTheme).toHaveBeenCalledWith('system');
+ });
+ });
+
+ describe('Active Theme Indicator', () => {
+ it('shows checkmark for light theme when active', async () => {
+ const user = userEvent.setup();
+
+ (useTheme as jest.Mock).mockReturnValue({
+ theme: 'light',
+ setTheme: mockSetTheme,
+ resolvedTheme: 'light',
+ });
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ const lightOption = await screen.findByRole('menuitem', { name: /light/i });
+ expect(lightOption).toHaveTextContent('✓');
+ });
+
+ it('shows checkmark for dark theme when active', async () => {
+ const user = userEvent.setup();
+
+ (useTheme as jest.Mock).mockReturnValue({
+ theme: 'dark',
+ setTheme: mockSetTheme,
+ resolvedTheme: 'dark',
+ });
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ const darkOption = await screen.findByRole('menuitem', { name: /dark/i });
+ expect(darkOption).toHaveTextContent('✓');
+ });
+
+ it('shows checkmark for system theme when active', async () => {
+ const user = userEvent.setup();
+
+ (useTheme as jest.Mock).mockReturnValue({
+ theme: 'system',
+ setTheme: mockSetTheme,
+ resolvedTheme: 'light',
+ });
+
+ render();
+
+ const button = screen.getByRole('button', { name: /toggle theme/i });
+ await user.click(button);
+
+ const systemOption = await screen.findByRole('menuitem', { name: /system/i });
+ expect(systemOption).toHaveTextContent('✓');
+ });
+ });
+
+});