Files
rentall-app/frontend/src/__tests__/components/Navbar.test.tsx
2025-12-20 14:59:09 -05:00

269 lines
8.3 KiB
TypeScript

/**
* Navbar Component Tests
*
* Tests for the Navbar component including navigation links,
* user authentication state, search functionality, and notifications.
*/
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Navbar from '../../components/Navbar';
import { rentalAPI, messageAPI } from '../../services/api';
// Mock dependencies
jest.mock('../../services/api', () => ({
rentalAPI: {
getPendingRequestsCount: jest.fn(),
},
messageAPI: {
getUnreadCount: jest.fn(),
},
}));
// Mock socket context
jest.mock('../../contexts/SocketContext', () => ({
useSocket: () => ({
onNewMessage: jest.fn(() => () => {}),
onMessageRead: jest.fn(() => () => {}),
}),
}));
// Variable to control auth state per test
let mockUser: any = null;
const mockLogout = jest.fn();
const mockOpenAuthModal = jest.fn();
jest.mock('../../contexts/AuthContext', () => ({
useAuth: () => ({
user: mockUser,
logout: mockLogout,
openAuthModal: mockOpenAuthModal,
}),
}));
// Mock useNavigate
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));
// Helper to render with Router
const renderWithRouter = (component: React.ReactElement) => {
return render(<BrowserRouter>{component}</BrowserRouter>);
};
describe('Navbar', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUser = null;
// Default mock implementations
(rentalAPI.getPendingRequestsCount as jest.Mock).mockResolvedValue({ data: { count: 0 } });
(messageAPI.getUnreadCount as jest.Mock).mockResolvedValue({ data: { count: 0 } });
});
describe('Branding', () => {
it('should display the brand name', () => {
renderWithRouter(<Navbar />);
expect(screen.getByText('CommunityRentals.App')).toBeInTheDocument();
});
it('should link brand to home page', () => {
renderWithRouter(<Navbar />);
const brandLink = screen.getByRole('link', { name: /CommunityRentals.App/i });
expect(brandLink).toHaveAttribute('href', '/');
});
});
describe('Search Functionality', () => {
it('should render search input', () => {
renderWithRouter(<Navbar />);
expect(screen.getByPlaceholderText('Search items...')).toBeInTheDocument();
});
it('should render location input', () => {
renderWithRouter(<Navbar />);
expect(screen.getByPlaceholderText('City or ZIP')).toBeInTheDocument();
});
it('should render search button', () => {
renderWithRouter(<Navbar />);
// Search button has an icon
const searchButton = document.querySelector('.bi-search');
expect(searchButton).toBeInTheDocument();
});
it('should navigate to items page when search is submitted', () => {
renderWithRouter(<Navbar />);
const searchInput = screen.getByPlaceholderText('Search items...');
fireEvent.change(searchInput, { target: { value: 'camera' } });
const searchButton = document.querySelector('button.btn-outline-secondary') as HTMLButtonElement;
fireEvent.click(searchButton);
expect(mockNavigate).toHaveBeenCalledWith('/items?search=camera');
});
it('should handle Enter key in search input', () => {
renderWithRouter(<Navbar />);
const searchInput = screen.getByPlaceholderText('Search items...');
fireEvent.change(searchInput, { target: { value: 'tent' } });
fireEvent.keyDown(searchInput, { key: 'Enter' });
expect(mockNavigate).toHaveBeenCalledWith('/items?search=tent');
});
it('should append city to search URL', () => {
renderWithRouter(<Navbar />);
const searchInput = screen.getByPlaceholderText('Search items...');
const locationInput = screen.getByPlaceholderText('City or ZIP');
fireEvent.change(searchInput, { target: { value: 'kayak' } });
fireEvent.change(locationInput, { target: { value: 'Seattle' } });
const searchButton = document.querySelector('button.btn-outline-secondary') as HTMLButtonElement;
fireEvent.click(searchButton);
expect(mockNavigate).toHaveBeenCalledWith('/items?search=kayak&city=Seattle');
});
it('should append zipCode when location is a zip code', () => {
renderWithRouter(<Navbar />);
const searchInput = screen.getByPlaceholderText('Search items...');
const locationInput = screen.getByPlaceholderText('City or ZIP');
fireEvent.change(searchInput, { target: { value: 'bike' } });
fireEvent.change(locationInput, { target: { value: '98101' } });
const searchButton = document.querySelector('button.btn-outline-secondary') as HTMLButtonElement;
fireEvent.click(searchButton);
expect(mockNavigate).toHaveBeenCalledWith('/items?search=bike&zipCode=98101');
});
it('should clear search fields after search', () => {
renderWithRouter(<Navbar />);
const searchInput = screen.getByPlaceholderText('Search items...') as HTMLInputElement;
fireEvent.change(searchInput, { target: { value: 'camera' } });
const searchButton = document.querySelector('button.btn-outline-secondary') as HTMLButtonElement;
fireEvent.click(searchButton);
expect(searchInput.value).toBe('');
});
});
describe('Logged Out State', () => {
it('should show login button when user is not logged in', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('button', { name: 'Login or Sign Up' })).toBeInTheDocument();
});
it('should call openAuthModal when login button is clicked', () => {
renderWithRouter(<Navbar />);
fireEvent.click(screen.getByRole('button', { name: 'Login or Sign Up' }));
expect(mockOpenAuthModal).toHaveBeenCalledWith('login');
});
});
describe('Logged In State', () => {
beforeEach(() => {
mockUser = {
id: 'user-123',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
};
});
it('should show user name when logged in', () => {
renderWithRouter(<Navbar />);
expect(screen.getByText('John')).toBeInTheDocument();
});
it('should not show login button when logged in', () => {
renderWithRouter(<Navbar />);
expect(screen.queryByRole('button', { name: 'Login or Sign Up' })).not.toBeInTheDocument();
});
it('should show profile link in dropdown', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: /Profile/i })).toHaveAttribute('href', '/profile');
});
it('should show renting link in dropdown', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: /Renting/i })).toHaveAttribute('href', '/renting');
});
it('should show owning link in dropdown', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: /Owning/i })).toHaveAttribute('href', '/owning');
});
it('should show messages link in dropdown', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: /Messages/i })).toHaveAttribute('href', '/messages');
});
it('should show forum link in dropdown', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: /Forum/i })).toHaveAttribute('href', '/forum');
});
it('should show earnings link in dropdown', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: /Earnings/i })).toHaveAttribute('href', '/earnings');
});
it('should call logout and navigate home when logout is clicked', () => {
renderWithRouter(<Navbar />);
const logoutButton = screen.getByRole('button', { name: /Logout/i });
fireEvent.click(logoutButton);
expect(mockLogout).toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith('/');
});
});
describe('Start Earning Link', () => {
it('should show Start Earning link', () => {
renderWithRouter(<Navbar />);
expect(screen.getByRole('link', { name: 'Start Earning' })).toHaveAttribute('href', '/create-item');
});
});
describe('Mobile Navigation', () => {
it('should render mobile toggle button', () => {
renderWithRouter(<Navbar />);
expect(screen.getByLabelText('Toggle navigation')).toBeInTheDocument();
});
});
});