7416a925a6
- Kopfzeile bekommt Einhaengepunkt header-center-slot; auf / entfaellt der Text Startseite - DashboardTabs rendert per createPortal dorthin, eigene Zeile ueber dem Raster entfaellt - eingelassene Spur mit erhabenem aktivem Reiter, waagrecht scrollbar mit weicher Randausblendung - role=tablist/tab, Pfeiltasten/Pos1/Ende, sichtbarer Fokusring, aktiver Reiter wird ins Bild gescrollt - Plus-Knopf ausserhalb der Spur, Loeschdialog am Dokumentkoerper, Ziehhinweis als sr-only/Tooltip Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
181 lines
5.6 KiB
TypeScript
181 lines
5.6 KiB
TypeScript
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<unknown>(
|
|
(o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined),
|
|
messages,
|
|
) as string | undefined;
|
|
return {
|
|
useTranslations:
|
|
(ns?: string) =>
|
|
(key: string, values?: Record<string, unknown>) => {
|
|
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;
|
|
}) => (
|
|
<a href={href} className={className}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}));
|
|
|
|
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(<Header />);
|
|
}
|
|
|
|
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());
|
|
});
|
|
});
|