/** * AppHeader.test.tsx * Unit tests for the AppHeader component * Covers: page titles, navigation links, active state, scoring card, * logout button, mobile hamburger menu toggle, and accessibility. */ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import { vi } from 'vitest'; import AppHeader from '../../src/components/AppHeader'; const mockOnLogout = vi.fn(); const renderAppHeader = (props: React.ComponentProps) => { return render( ); }; describe('AppHeader Component', () => { beforeEach(() => { mockOnLogout.mockClear(); }); it('should render dashboard page title and icon', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); // Find the h1 element which contains the page title const heading = screen.getByRole('heading', { name: /dashboard/i }); expect(heading).toBeInTheDocument(); }); it('should render quests page title and icon', () => { renderAppHeader({ page: 'quests', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const heading = screen.getByRole('heading', { name: /quests/i }); expect(heading).toBeInTheDocument(); }); it('should render map page title and icon', () => { renderAppHeader({ page: 'map', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const heading = screen.getByRole('heading', { name: /middle-earth map/i }); expect(heading).toBeInTheDocument(); }); it('should render inventory page title and icon', () => { renderAppHeader({ page: 'inventory', gold: 100, completedQuests: 5, averageSavings: 15.5, onLogout: mockOnLogout, }); const heading = screen.getByRole('heading', { name: /inventory/i }); expect(heading).toBeInTheDocument(); }); it('should render navigation links', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); expect(screen.getByTestId('header-nav-dashboard')).toBeInTheDocument(); expect(screen.getByTestId('header-nav-quests')).toBeInTheDocument(); expect(screen.getByTestId('header-nav-map')).toBeInTheDocument(); expect(screen.getByTestId('header-nav-inventory')).toBeInTheDocument(); }); it('should render logout button', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const logoutButton = screen.getByTestId('header-logout-button'); expect(logoutButton).toBeInTheDocument(); expect(logoutButton).toHaveTextContent('Leave Fellowship'); }); it('should highlight current page link', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const dashboardLink = screen.getByTestId('header-nav-dashboard'); // The dashboard link should have different styling when active expect(dashboardLink).toHaveClass('font-bold', 'text-gold'); }); it('should render ScoringCard with correct props', () => { renderAppHeader({ page: 'dashboard', gold: 500, completedQuests: 10, averageSavings: 20.5, onLogout: mockOnLogout, }); // Check that ScoringCard is rendered with correct values expect(screen.getByTestId('scoring-card')).toBeInTheDocument(); expect(screen.getByTestId('scoring-card-gold')).toHaveTextContent('500'); expect(screen.getByTestId('scoring-card-quests')).toHaveTextContent('10'); expect(screen.getByTestId('scoring-card-savings')).toHaveTextContent('20.50%'); }); it('should render ScoringCard without average savings on non-inventory pages', () => { renderAppHeader({ page: 'quests', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); expect(screen.getByTestId('scoring-card-savings')).toHaveTextContent('—'); }); it('should have navigation element with correct testId', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); expect(screen.getByTestId('header-navigation')).toBeInTheDocument(); }); it('should render different page titles for different pages', () => { const { rerender } = render( ); let heading = screen.getByRole('heading', { name: /dashboard/i }); expect(heading).toBeInTheDocument(); rerender( ); heading = screen.getByRole('heading', { name: /inventory/i }); expect(heading).toBeInTheDocument(); }); // ── Mobile Hamburger Menu ────────────────────────────────────────────────── it('should render the mobile hamburger button', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const hamburger = screen.getByTestId('header-mobile-menu-btn'); expect(hamburger).toBeInTheDocument(); expect(hamburger).toHaveAttribute('aria-expanded', 'false'); }); it('should toggle aria-expanded on hamburger click', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const hamburger = screen.getByTestId('header-mobile-menu-btn'); // Initially closed expect(hamburger).toHaveAttribute('aria-expanded', 'false'); // Open fireEvent.click(hamburger); expect(hamburger).toHaveAttribute('aria-expanded', 'true'); // Close again fireEvent.click(hamburger); expect(hamburger).toHaveAttribute('aria-expanded', 'false'); }); it('should add lotr-hamburger--open class when menu is open', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const hamburger = screen.getByTestId('header-mobile-menu-btn'); expect(hamburger).not.toHaveClass('lotr-hamburger--open'); fireEvent.click(hamburger); expect(hamburger).toHaveClass('lotr-hamburger--open'); }); it('should apply lotr-header--menu-open class to header when hamburger is open', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const hamburger = screen.getByTestId('header-mobile-menu-btn'); const header = screen.getByRole('banner'); expect(header).not.toHaveClass('lotr-header--menu-open'); fireEvent.click(hamburger); expect(header).toHaveClass('lotr-header--menu-open'); }); it('should close mobile menu when a nav link is clicked', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const hamburger = screen.getByTestId('header-mobile-menu-btn'); fireEvent.click(hamburger); expect(hamburger).toHaveAttribute('aria-expanded', 'true'); // Click a nav link → should close the menu const questsLink = screen.getByTestId('header-nav-quests'); fireEvent.click(questsLink); expect(hamburger).toHaveAttribute('aria-expanded', 'false'); }); // ── Fantasy Branding ────────────────────────────────────────────────────── it('should render the brand ring icon SVG', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); // The ring SVG is inside the header const header = screen.getByRole('banner'); const rings = header.querySelectorAll('svg'); // At minimum two SVGs: ring icon + elvish border expect(rings.length).toBeGreaterThanOrEqual(2); }); it('should have aria-label on the hamburger for accessibility', () => { renderAppHeader({ page: 'dashboard', gold: 100, completedQuests: 5, averageSavings: undefined, onLogout: mockOnLogout, }); const hamburger = screen.getByTestId('header-mobile-menu-btn'); expect(hamburger).toHaveAttribute('aria-label', 'Open navigation menu'); fireEvent.click(hamburger); expect(hamburger).toHaveAttribute('aria-label', 'Close navigation menu'); }); });