lotr-sut/sut/frontend/test/components/AppHeader.test.tsx
Fellowship Scholar f6a5823439 init commit
2026-03-29 20:07:56 +00:00

323 lines
9.3 KiB
TypeScript

/**
* 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<typeof AppHeader>) => {
return render(
<BrowserRouter>
<AppHeader {...props} />
</BrowserRouter>
);
};
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(
<BrowserRouter>
<AppHeader
page="dashboard"
gold={100}
completedQuests={5}
averageSavings={undefined}
onLogout={mockOnLogout}
/>
</BrowserRouter>
);
let heading = screen.getByRole('heading', { name: /dashboard/i });
expect(heading).toBeInTheDocument();
rerender(
<BrowserRouter>
<AppHeader
page="inventory"
gold={100}
completedQuests={5}
averageSavings={15.5}
onLogout={mockOnLogout}
/>
</BrowserRouter>
);
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');
});
});