Files
syndarix/frontend/tests/components/layout/Header.test.tsx
Felipe Cardoso a79d923dc1 test(frontend): improve test coverage and update edge case handling
- Refactor tests to handle empty `model_params` in AgentTypeForm.
- Add return type annotations (`: never`) for throwing functions in ErrorBoundary tests.
- Mock `useAuth` in home page tests for consistent auth state handling.
- Update Header test to validate updated `/dashboard` link.
2026-01-03 01:19:35 +01:00

347 lines
9.7 KiB
TypeScript

/**
* Tests for Header Component
* Verifies navigation, user menu, and auth-based rendering
*/
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Header } from '@/components/layout/Header';
import { useAuth } from '@/lib/auth/AuthContext';
import { useLogout } from '@/lib/api/hooks/useAuth';
import { mockUsePathname } from 'next-intl/navigation';
import type { User } from '@/lib/stores/authStore';
// Mock dependencies
jest.mock('@/lib/auth/AuthContext', () => ({
useAuth: jest.fn(),
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
jest.mock('@/lib/api/hooks/useAuth', () => ({
useLogout: jest.fn(),
}));
jest.mock('@/components/theme', () => ({
ThemeToggle: () => <div data-testid="theme-toggle">Theme Toggle</div>,
}));
// Helper to create mock user
function createMockUser(overrides: Partial<User> = {}): User {
return {
id: 'user-123',
email: 'test@example.com',
first_name: 'Test',
last_name: 'User',
phone_number: null,
is_active: true,
is_superuser: false,
created_at: new Date().toISOString(),
updated_at: null,
...overrides,
};
}
describe('Header', () => {
const mockLogout = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockUsePathname.mockReturnValue('/');
(useLogout as jest.Mock).mockReturnValue({
mutate: mockLogout,
isPending: false,
});
});
describe('Rendering', () => {
it('renders header with logo', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
expect(screen.getByText('Syndarix')).toBeInTheDocument();
});
it('renders theme toggle', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
expect(screen.getByTestId('theme-toggle')).toBeInTheDocument();
});
it('renders user avatar with initials', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: 'Doe',
}),
});
render(<Header />);
expect(screen.getByText('JD')).toBeInTheDocument();
});
it('renders user avatar with single initial when no last name', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: null,
}),
});
render(<Header />);
expect(screen.getByText('J')).toBeInTheDocument();
});
it('renders default initial when no first name', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: '',
}),
});
render(<Header />);
expect(screen.getByText('U')).toBeInTheDocument();
});
});
describe('Navigation Links', () => {
it('renders home link', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
const homeLink = screen.getByRole('link', { name: /home/i });
expect(homeLink).toHaveAttribute('href', '/dashboard');
});
it('renders admin link for superusers', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: true }),
});
render(<Header />);
const adminLink = screen.getByRole('link', { name: /admin/i });
expect(adminLink).toHaveAttribute('href', '/admin');
});
it('does not render admin link for regular users', () => {
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: false }),
});
render(<Header />);
const adminLinks = screen.queryAllByRole('link', { name: /admin/i });
// Filter out the one in the dropdown menu
const navAdminLinks = adminLinks.filter((link) => !link.closest('[role="menu"]'));
expect(navAdminLinks).toHaveLength(0);
});
it('highlights active navigation link', () => {
mockUsePathname.mockReturnValue('/admin');
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: true }),
});
render(<Header />);
const adminLink = screen.getByRole('link', { name: /admin/i });
expect(adminLink).toHaveClass('bg-primary');
});
});
describe('User Dropdown Menu', () => {
it('opens dropdown when avatar is clicked', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
}),
});
render(<Header />);
// Find avatar button by looking for the button containing the avatar initials
const avatarButton = screen.getByText('JD').closest('button')!;
await user.click(avatarButton);
await waitFor(() => {
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
});
it('displays user info in dropdown', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({
first_name: 'John',
last_name: 'Doe',
email: 'john@example.com',
}),
});
render(<Header />);
const avatarButton = screen.getByText('JD').closest('button')!;
await user.click(avatarButton);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
});
it('includes profile link in dropdown', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
const profileLink = await screen.findByRole('menuitem', { name: /profile/i });
expect(profileLink).toHaveAttribute('href', '/settings/profile');
});
it('includes settings link in dropdown', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
const settingsLink = await screen.findByRole('menuitem', { name: /settings/i });
expect(settingsLink).toHaveAttribute('href', '/settings/password');
});
it('includes admin panel link for superusers', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: true }),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
const adminLink = await screen.findByRole('menuitem', { name: /admin/i });
expect(adminLink).toHaveAttribute('href', '/admin');
});
it('does not include admin panel link for regular users', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser({ is_superuser: false }),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
await waitFor(() => {
// Only check for a link to /admin since "Admin" text might appear in navigation
const adminMenuLinks = screen.queryAllByRole('menuitem', { name: /admin/i });
const adminLinkInMenu = adminMenuLinks.find(
(link) => link.getAttribute('href') === '/admin'
);
expect(adminLinkInMenu).toBeUndefined();
});
});
});
describe('Logout Functionality', () => {
it('calls logout when logout button is clicked', async () => {
const user = userEvent.setup();
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
const logoutButton = await screen.findByRole('menuitem', { name: /logout/i });
await user.click(logoutButton);
expect(mockLogout).toHaveBeenCalledTimes(1);
});
it('shows loading state when logging out', async () => {
const user = userEvent.setup();
(useLogout as jest.Mock).mockReturnValue({
mutate: mockLogout,
isPending: true,
});
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
await waitFor(() => {
// i18n key shown as literal when translation isn't found
expect(screen.getByText('loggingOut')).toBeInTheDocument();
});
});
it('disables logout button when logging out', async () => {
const user = userEvent.setup();
(useLogout as jest.Mock).mockReturnValue({
mutate: mockLogout,
isPending: true,
});
(useAuth as unknown as jest.Mock).mockReturnValue({
user: createMockUser(),
});
render(<Header />);
const avatarButton = screen.getByText('TU').closest('button')!;
await user.click(avatarButton);
const logoutButton = await screen.findByRole('menuitem', { name: /loggingOut/i });
expect(logoutButton).toHaveAttribute('data-disabled');
});
});
});