import { cleanup, render, screen, waitFor } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { useAuthStore } from '@/lib/stores/auth-store'; /** * header.test — Sitzungswaechter im Header (quick-260917-gyd). * * next-intl de.json-gestuetzt (Muster: account-settings-form.test.tsx), * fetchSessionState/logout aus @/lib/auth-actions per vi.hoisted gemockt, * usePathname per mutable Variable (Muster: sidebar.test.tsx). Der echte * useAuthStore wird verwendet (nicht gemockt) und in afterEach * zurueckgesetzt. */ vi.mock('next-intl', async () => { const messages = (await import('@/messages/de.json')).default as Record< string, unknown >; const lookup = (path: string): string | undefined => path .split('.') .reduce( (o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined), messages, ) as string | undefined; return { useTranslations: (ns?: string) => (key: string, values?: Record) => { const raw = lookup(ns ? `${ns}.${key}` : key) ?? key; return values ? raw.replace(/\{(\w+)\}/g, (_: string, n: string) => String(values[n] ?? ''), ) : raw; }, useLocale: () => 'de', }; }); let mockPathname = '/settings/general/desktop'; vi.mock('next/navigation', () => ({ usePathname: () => mockPathname, })); vi.mock('next/link', () => ({ default: ({ children, href, className, }: { children: React.ReactNode; href: string; className?: string; }) => ( {children} ), })); const { fetchSessionState, logout } = vi.hoisted(() => ({ fetchSessionState: vi.fn(), logout: vi.fn(), })); vi.mock('@/lib/auth-actions', () => ({ fetchSessionState, logout })); vi.mock('@/components/bug-report/bug-report-button', () => ({ BugReportButton: () => null, })); vi.mock('@/components/theme-toggle', () => ({ ThemeToggle: () => null, })); vi.mock('@/components/brand/tessera-logo', () => ({ TesseraLogo: () => null, })); afterEach(() => { cleanup(); vi.unstubAllGlobals(); useAuthStore.setState({ user: null }); vi.clearAllMocks(); mockPathname = '/settings/general/desktop'; }); async function renderHeader() { const { Header } = await import('./header'); render(
); } describe('Header — Sitzungswaechter (quick-260917-gyd)', () => { it('Test 1 (authenticated): Store enthaelt den Benutzer, kein Redirect', async () => { vi.stubGlobal('location', { href: '', pathname: '/settings/general/desktop', search: '' }); fetchSessionState.mockResolvedValue({ status: 'authenticated', user: { id: 'u1', username: 'schalli', displayName: 'Schalli', role: 'ADMIN', tenantId: 't1', hasAvatar: false, accentColor: null, }, }); await renderHeader(); await waitFor(() => expect(useAuthStore.getState().user?.username).toBe('schalli'), ); expect((window as any).location.href).toBe(''); }); it('Test 2 (unauthenticated auf Unterseite): Vollnavigation auf /login?next=…', async () => { vi.stubGlobal('location', { href: '', pathname: '/settings/general/desktop', search: '' }); fetchSessionState.mockResolvedValue({ status: 'unauthenticated' }); await renderHeader(); await waitFor(() => expect((window as any).location.href).toBe( '/login?next=%2Fsettings%2Fgeneral%2Fdesktop', ), ); }); it('Test 3 (unauthenticated auf /): Vollnavigation auf /login ohne next', async () => { mockPathname = '/'; vi.stubGlobal('location', { href: '', pathname: '/', search: '' }); fetchSessionState.mockResolvedValue({ status: 'unauthenticated' }); await renderHeader(); await waitFor(() => expect((window as any).location.href).toBe('/login')); }); it('Test 4 (unavailable): kein Redirect, Store bleibt leer, Avatar zeigt "?"', async () => { vi.stubGlobal('location', { href: '', pathname: '/settings/general/desktop', search: '' }); fetchSessionState.mockResolvedValue({ status: 'unavailable' }); await renderHeader(); await waitFor(() => expect(fetchSessionState).toHaveBeenCalledTimes(1)); expect((window as any).location.href).toBe(''); expect(useAuthStore.getState().user).toBeNull(); expect( screen.getByRole('button', { name: 'Benutzermenu' }), ).toHaveTextContent('?'); }); }); describe('Header — Mitte der Kopfzeile (quick-260924-h7x)', () => { it('auf der Startseite: kein Text „Startseite“, aber der leere Einhaengepunkt fuer die Reiter', async () => { mockPathname = '/'; vi.stubGlobal('location', { href: '', pathname: '/', search: '' }); fetchSessionState.mockResolvedValue({ status: 'unavailable' }); await renderHeader(); expect(screen.queryByText('Startseite')).not.toBeInTheDocument(); const slot = document.getElementById('header-center-slot'); expect(slot).not.toBeNull(); expect(slot).toBeEmptyDOMElement(); await waitFor(() => expect(fetchSessionState).toHaveBeenCalled()); }); it('auf anderen Seiten: „Startseite“ bleibt wie bisher', async () => { mockPathname = '/modules/proxmox'; vi.stubGlobal('location', { href: '', pathname: '/modules/proxmox', search: '' }); fetchSessionState.mockResolvedValue({ status: 'unavailable' }); await renderHeader(); expect(screen.getByText('Startseite')).toBeInTheDocument(); expect(document.getElementById('header-center-slot')).not.toBeNull(); await waitFor(() => expect(fetchSessionState).toHaveBeenCalled()); }); });