/** * Component: Header Component Tests * Documentation: documentation/frontend/components.md */ // @vitest-environment jsdom import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../helpers/render'; describe('Header', () => { let Header: typeof import('@/components/layout/Header').Header; beforeAll(async () => { ({ Header } = await import('@/components/layout/Header')); }); beforeEach(() => { localStorage.clear(); }); afterEach(() => { vi.unstubAllGlobals(); }); it('renders login button and opens Plex auth window', async () => { const fetchMock = vi.fn().mockImplementation((input: RequestInfo) => { if (input === '/api/version') { return Promise.resolve({ json: vi.fn().mockResolvedValue({ version: 'v.test' }), }); } return Promise.resolve({ json: vi.fn().mockResolvedValue({ success: true, authUrl: 'https://plex.example/login' }), }); }); const openMock = vi.spyOn(window, 'open').mockImplementation(() => null); vi.stubGlobal('fetch', fetchMock); renderWithProviders(
, { auth: { user: null, isLoading: false } }); const loginButton = screen.getByRole('button', { name: /login with plex/i }); await userEvent.click(loginButton); await waitFor(() => { expect(fetchMock).toHaveBeenCalledWith('/api/auth/plex/login', { method: 'POST' }); }); expect(openMock).toHaveBeenCalledWith( 'https://plex.example/login', 'plex-auth', 'width=600,height=700' ); }); it('renders admin navigation and user menu actions for local users', async () => { const fetchMock = vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ version: 'v.test' }), }); vi.stubGlobal('fetch', fetchMock); renderWithProviders(
, { auth: { user: { id: 'admin-1', plexId: 'plex-1', username: 'admin', role: 'admin', authProvider: 'local', }, isLoading: false, }, }); expect(screen.getByRole('link', { name: 'Admin' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'My Requests' })).toBeInTheDocument(); const userButton = screen.getByText('admin').closest('button'); expect(userButton).not.toBeNull(); await userEvent.click(userButton as HTMLButtonElement); await waitFor(() => { expect(screen.getByText('Change Password')).toBeInTheDocument(); }); expect(screen.getByText('Logout')).toBeInTheDocument(); }); it('shows BookDate link and avatar when BookDate is enabled', async () => { localStorage.setItem('accessToken', 'token'); const fetchMock = vi.fn().mockImplementation((input: RequestInfo) => { if (input === '/api/version') { return Promise.resolve({ json: vi.fn().mockResolvedValue({ version: 'v.test' }), }); } if (input === '/api/bookdate/config') { return Promise.resolve({ json: vi.fn().mockResolvedValue({ config: { isVerified: true, isEnabled: true }, }), }); } return Promise.resolve({ json: vi.fn().mockResolvedValue({}), }); }); vi.stubGlobal('fetch', fetchMock); renderWithProviders(
, { auth: { user: { id: 'user-1', plexId: 'plex-1', username: 'reader', role: 'user', avatarUrl: '/avatar.png', }, isLoading: false, }, }); await waitFor(() => { expect(screen.getByRole('link', { name: 'BookDate' })).toBeInTheDocument(); }); expect(screen.getByAltText('reader')).toBeInTheDocument(); }); it('logs out from the user menu and shows initials fallback', async () => { const fetchMock = vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ version: 'v.test' }), }); const logoutMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); renderWithProviders(
, { auth: { user: { id: 'user-2', plexId: 'plex-2', username: 'alice', role: 'user', authProvider: 'plex', }, logout: logoutMock, isLoading: false, }, }); expect(screen.getByText(/^A$/)).toBeInTheDocument(); const userButton = screen.getByText('alice').closest('button'); expect(userButton).not.toBeNull(); await userEvent.click(userButton as HTMLButtonElement); await waitFor(() => { expect(screen.getByText('Logout')).toBeInTheDocument(); }); await userEvent.click(screen.getByText('Logout')); expect(logoutMock).toHaveBeenCalledTimes(1); }); it('toggles the mobile menu and closes after navigation', async () => { const fetchMock = vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ version: 'v.test' }), }); vi.stubGlobal('fetch', fetchMock); renderWithProviders(
, { auth: { user: null, isLoading: false } }); const initialHomeLinks = screen.getAllByRole('link', { name: 'Home' }).length; await userEvent.click(screen.getByRole('button', { name: 'Toggle menu' })); const openHomeLinks = screen.getAllByRole('link', { name: 'Home' }); expect(openHomeLinks).toHaveLength(initialHomeLinks + 1); await userEvent.click(openHomeLinks[openHomeLinks.length - 1]); expect(screen.getAllByRole('link', { name: 'Home' })).toHaveLength(initialHomeLinks); }); it('hides BookDate when config check fails', async () => { localStorage.setItem('accessToken', 'token'); const errorMock = vi.spyOn(console, 'error').mockImplementation(() => undefined); const fetchMock = vi.fn().mockImplementation((input: RequestInfo) => { if (input === '/api/version') { return Promise.resolve({ json: vi.fn().mockResolvedValue({ version: 'v.test' }), }); } if (input === '/api/bookdate/config') { return Promise.reject(new Error('boom')); } return Promise.resolve({ json: vi.fn().mockResolvedValue({}), }); }); vi.stubGlobal('fetch', fetchMock); renderWithProviders(
, { auth: { user: { id: 'user-3', plexId: 'plex-3', username: 'reader', role: 'user', }, isLoading: false, }, }); await waitFor(() => { expect(errorMock).toHaveBeenCalledWith('Failed to check BookDate config:', expect.any(Error)); }); expect(screen.queryByRole('link', { name: 'BookDate' })).not.toBeInTheDocument(); }); });