tududi/frontend/__tests__/components/Navbar.test.tsx
Chris 3c1209a5a9
Express migration (#80)
* Initial migration

* Cleanup and create migration scripts

* Introduce test suite

* Fix test issues

* Correct CORS issue and update paths

* Update README
2025-06-16 21:50:44 +03:00

286 lines
No EOL
7.4 KiB
TypeScript

import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import Navbar from '../../components/Navbar';
const mockUser: {
email: string;
avatarUrl?: string;
} = {
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.jpg'
};
const defaultProps = {
isDarkMode: false,
toggleDarkMode: jest.fn(),
currentUser: mockUser,
setCurrentUser: jest.fn(),
isSidebarOpen: false,
setIsSidebarOpen: jest.fn()
};
const renderNavbar = (props = defaultProps) => {
return render(
<BrowserRouter>
<Navbar {...props} />
</BrowserRouter>
);
};
// Mock logout API call
global.fetch = jest.fn();
describe('Navbar Component', () => {
beforeEach(() => {
jest.clearAllMocks();
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => ({ success: true })
});
});
it('renders navbar with user email', () => {
renderNavbar();
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
it('renders dark mode toggle button', () => {
renderNavbar();
const darkModeButton = screen.getByRole('button', { name: /toggle dark mode/i });
expect(darkModeButton).toBeInTheDocument();
});
it('calls toggleDarkMode when dark mode button is clicked', async () => {
const user = userEvent.setup();
const mockToggleDarkMode = jest.fn();
renderNavbar({
...defaultProps,
toggleDarkMode: mockToggleDarkMode
});
const darkModeButton = screen.getByRole('button', { name: /toggle dark mode/i });
await act(async () => {
await user.click(darkModeButton);
});
expect(mockToggleDarkMode).toHaveBeenCalled();
});
it('renders sidebar toggle button', () => {
renderNavbar();
const sidebarButton = screen.getByRole('button', { name: /toggle sidebar/i });
expect(sidebarButton).toBeInTheDocument();
});
it('calls setIsSidebarOpen when sidebar button is clicked', async () => {
const user = userEvent.setup();
const mockSetIsSidebarOpen = jest.fn();
renderNavbar({
...defaultProps,
setIsSidebarOpen: mockSetIsSidebarOpen
});
const sidebarButton = screen.getByRole('button', { name: /toggle sidebar/i });
await act(async () => {
await user.click(sidebarButton);
});
expect(mockSetIsSidebarOpen).toHaveBeenCalledWith(true);
});
it('opens user dropdown when user icon is clicked', async () => {
const user = userEvent.setup();
renderNavbar();
const userButton = screen.getByRole('button', { name: /user menu/i });
await act(async () => {
await user.click(userButton);
});
await waitFor(() => {
expect(screen.getByText(/profile/i)).toBeInTheDocument();
expect(screen.getByText(/logout/i)).toBeInTheDocument();
});
});
it('closes dropdown when clicking outside', async () => {
const user = userEvent.setup();
renderNavbar();
const userButton = screen.getByRole('button', { name: /user menu/i });
await act(async () => {
await user.click(userButton);
});
await waitFor(() => {
expect(screen.getByText(/logout/i)).toBeInTheDocument();
});
// Click outside the dropdown
await act(async () => {
await user.click(document.body);
});
await waitFor(() => {
expect(screen.queryByText(/logout/i)).not.toBeInTheDocument();
});
});
it('handles logout when logout button is clicked', async () => {
const user = userEvent.setup();
const mockSetCurrentUser = jest.fn();
renderNavbar({
...defaultProps,
setCurrentUser: mockSetCurrentUser
});
const userButton = screen.getByRole('button', { name: /user menu/i });
await act(async () => {
await user.click(userButton);
});
await waitFor(() => {
expect(screen.getByText(/logout/i)).toBeInTheDocument();
});
const logoutButton = screen.getByText(/logout/i);
await act(async () => {
await user.click(logoutButton);
});
await waitFor(() => {
expect(global.fetch).toHaveBeenCalledWith('/api/logout', {
method: 'POST',
credentials: 'include'
});
});
});
it('renders tududi brand link', () => {
renderNavbar();
const brandLink = screen.getByRole('link', { name: /tududi/i });
expect(brandLink).toBeInTheDocument();
expect(brandLink).toHaveAttribute('href', '/');
});
it('displays correct dark mode icon based on current mode', () => {
// Test light mode (should show moon icon)
const { rerender } = renderNavbar({
...defaultProps,
isDarkMode: false
});
expect(screen.getByRole('button', { name: /toggle dark mode/i })).toBeInTheDocument();
// Test dark mode (should show sun icon)
rerender(
<BrowserRouter>
<Navbar {...defaultProps} isDarkMode={true} />
</BrowserRouter>
);
expect(screen.getByRole('button', { name: /toggle dark mode/i })).toBeInTheDocument();
});
it('handles user without avatar', () => {
const userWithoutAvatar = {
email: 'test@example.com',
avatarUrl: undefined
};
renderNavbar({
...defaultProps,
currentUser: userWithoutAvatar
});
expect(screen.getByText('test@example.com')).toBeInTheDocument();
});
it('shows sidebar as open when isSidebarOpen is true', () => {
renderNavbar({
...defaultProps,
isSidebarOpen: true
});
const sidebarButton = screen.getByRole('button', { name: /toggle sidebar/i });
expect(sidebarButton).toBeInTheDocument();
});
it('handles logout error gracefully', async () => {
const user = userEvent.setup();
(global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
renderNavbar();
const userButton = screen.getByRole('button', { name: /user menu/i });
await act(async () => {
await user.click(userButton);
});
const logoutButton = screen.getByText(/logout/i);
await act(async () => {
await user.click(logoutButton);
});
// Should still attempt logout despite error
expect(global.fetch).toHaveBeenCalled();
});
it('navigates to profile when profile link is clicked', async () => {
const user = userEvent.setup();
renderNavbar();
const userButton = screen.getByRole('button', { name: /user menu/i });
await act(async () => {
await user.click(userButton);
});
await waitFor(() => {
expect(screen.getByText(/profile/i)).toBeInTheDocument();
});
const profileLink = screen.getByText(/profile/i);
expect(profileLink.closest('a')).toHaveAttribute('href', '/profile');
});
it('maintains dropdown state correctly', async () => {
const user = userEvent.setup();
renderNavbar();
const userButton = screen.getByRole('button', { name: /user menu/i });
// Open dropdown
await act(async () => {
await user.click(userButton);
});
expect(screen.getByText(/logout/i)).toBeInTheDocument();
// Close dropdown by clicking button again
await act(async () => {
await user.click(userButton);
});
await waitFor(() => {
expect(screen.queryByText(/logout/i)).not.toBeInTheDocument();
});
});
});