import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AuthContext, type AuthContextValue } from './AuthGate'; import { registerNavigationGuard } from './navigationGuard'; import { UserMenu } from './UserMenu'; function renderMenu(overrides: Partial = {}) { const signOut = overrides.signOut ?? vi.fn(); const value: AuthContextValue = { username: 'admin', method: 'local', signOut, requireLogin: vi.fn(), ...overrides }; render( ); return { signOut }; } describe('UserMenu', () => { beforeEach(() => { vi.restoreAllMocks(); }); afterEach(() => { cleanup(); }); it('shows the authenticated username', () => { renderMenu(); expect(screen.getByRole('button', { name: /admin/ })).toBeTruthy(); }); it('signs out: POSTs logout then calls AuthContext.signOut on 204', async () => { const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 })); const { signOut } = renderMenu(); fireEvent.click(screen.getByRole('button', { name: /admin/ })); fireEvent.click(screen.getByRole('button', { name: 'Sign out' })); await waitFor(() => expect(signOut).toHaveBeenCalledTimes(1)); expect(fetchMock).toHaveBeenCalledWith('/api/v1/auth/logout', expect.objectContaining({ method: 'POST' })); }); it('does not log out when the unsaved-changes guard vetoes', () => { const fetchMock = vi.spyOn(window, 'fetch'); const { signOut } = renderMenu(); const unregister = registerNavigationGuard(() => false); fireEvent.click(screen.getByRole('button', { name: /admin/ })); fireEvent.click(screen.getByRole('button', { name: 'Sign out' })); expect(fetchMock).not.toHaveBeenCalled(); expect(signOut).not.toHaveBeenCalled(); unregister(); }); });