/**
* Unit tests for TopBar component
*
* Tests the top navigation bar that appears at the top of the application.
* Covers:
* - Rendering of all UI elements (search, theme toggle, notifications, etc.)
* - Menu button for mobile view
* - Theme toggle functionality
* - User profile dropdown integration
* - Language selector integration
* - Notification dropdown integration
* - Sandbox toggle integration
* - Search input
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import TopBar from '../TopBar';
import { User } from '../../types';
// Mock child components
vi.mock('../UserProfileDropdown', () => ({
default: ({ user }: { user: User }) => (
User: {user.email}
),
}));
vi.mock('../LanguageSelector', () => ({
default: () => Language Selector
,
}));
vi.mock('../NotificationDropdown', () => ({
default: ({ onTicketClick }: { onTicketClick?: (id: string) => void }) => (
Notifications
),
}));
vi.mock('../SandboxToggle', () => ({
default: ({ isSandbox, sandboxEnabled, onToggle, isToggling }: any) => (
Sandbox: {isSandbox ? 'On' : 'Off'}
Toggle Sandbox
),
}));
// Mock SandboxContext
const mockUseSandbox = vi.fn();
vi.mock('../../contexts/SandboxContext', () => ({
useSandbox: () => mockUseSandbox(),
}));
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record = {
'common.search': 'Search...',
};
return translations[key] || key;
},
}),
}));
// Test data factory for User objects
const createMockUser = (overrides?: Partial): User => ({
id: '1',
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
role: 'owner',
phone: '+1234567890',
preferences: {
email: true,
sms: false,
in_app: true,
},
twoFactorEnabled: false,
profilePictureUrl: undefined,
...overrides,
});
// Wrapper component that provides router context
const renderWithRouter = (ui: React.ReactElement) => {
return render({ui} );
};
describe('TopBar', () => {
const mockToggleTheme = vi.fn();
const mockOnMenuClick = vi.fn();
const mockOnTicketClick = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
mockUseSandbox.mockReturnValue({
isSandbox: false,
sandboxEnabled: true,
toggleSandbox: vi.fn(),
isToggling: false,
});
});
describe('Rendering', () => {
it('should render the top bar with all main elements', () => {
const user = createMockUser();
renderWithRouter(
);
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
expect(screen.getByTestId('language-selector')).toBeInTheDocument();
expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument();
expect(screen.getByTestId('sandbox-toggle')).toBeInTheDocument();
});
it('should render search input on desktop', () => {
const user = createMockUser();
renderWithRouter(
);
const searchInput = screen.getByPlaceholderText('Search...');
expect(searchInput).toBeInTheDocument();
expect(searchInput).toHaveClass('w-full');
});
it('should render mobile menu button', () => {
const user = createMockUser();
renderWithRouter(
);
const menuButton = screen.getByLabelText('Open sidebar');
expect(menuButton).toBeInTheDocument();
});
it('should pass user to UserProfileDropdown', () => {
const user = createMockUser({ email: 'john@example.com' });
renderWithRouter(
);
expect(screen.getByText('User: john@example.com')).toBeInTheDocument();
});
it('should render with dark mode styles when isDarkMode is true', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
const header = container.querySelector('header');
expect(header).toHaveClass('dark:bg-gray-800', 'dark:border-gray-700');
});
});
describe('Theme Toggle', () => {
it('should render moon icon when in light mode', () => {
const user = createMockUser();
renderWithRouter(
);
// The button should exist
const buttons = screen.getAllByRole('button');
const themeButton = buttons.find(btn =>
btn.className.includes('text-gray-400')
);
expect(themeButton).toBeInTheDocument();
});
it('should render sun icon when in dark mode', () => {
const user = createMockUser();
renderWithRouter(
);
// The button should exist
const buttons = screen.getAllByRole('button');
const themeButton = buttons.find(btn =>
btn.className.includes('text-gray-400')
);
expect(themeButton).toBeInTheDocument();
});
it('should call toggleTheme when theme button is clicked', () => {
const user = createMockUser();
renderWithRouter(
);
// Find the theme toggle button by finding buttons, then clicking the one with the theme classes
const buttons = screen.getAllByRole('button');
// The theme button is the one with the hover styles and not the menu button
const themeButton = buttons.find(btn =>
btn.className.includes('text-gray-400') &&
btn.className.includes('hover:text-gray-600') &&
!btn.getAttribute('aria-label')
);
expect(themeButton).toBeTruthy();
if (themeButton) {
fireEvent.click(themeButton);
expect(mockToggleTheme).toHaveBeenCalledTimes(1);
}
});
});
describe('Mobile Menu Button', () => {
it('should render menu button with correct aria-label', () => {
const user = createMockUser();
renderWithRouter(
);
const menuButton = screen.getByLabelText('Open sidebar');
expect(menuButton).toHaveAttribute('aria-label', 'Open sidebar');
});
it('should call onMenuClick when menu button is clicked', () => {
const user = createMockUser();
renderWithRouter(
);
const menuButton = screen.getByLabelText('Open sidebar');
fireEvent.click(menuButton);
expect(mockOnMenuClick).toHaveBeenCalledTimes(1);
});
it('should have mobile-only classes on menu button', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
const menuButton = screen.getByLabelText('Open sidebar');
expect(menuButton).toHaveClass('md:hidden');
});
});
describe('Search Input', () => {
it('should render search input with correct placeholder', () => {
const user = createMockUser();
renderWithRouter(
);
const searchInput = screen.getByPlaceholderText('Search...');
expect(searchInput).toHaveAttribute('type', 'text');
});
it('should have search icon', () => {
const user = createMockUser();
renderWithRouter(
);
// Search icon should be present
const searchInput = screen.getByPlaceholderText('Search...');
expect(searchInput.parentElement?.querySelector('span')).toBeInTheDocument();
});
it('should allow typing in search input', () => {
const user = createMockUser();
renderWithRouter(
);
const searchInput = screen.getByPlaceholderText('Search...') as HTMLInputElement;
fireEvent.change(searchInput, { target: { value: 'test query' } });
expect(searchInput.value).toBe('test query');
});
it('should have focus styles on search input', () => {
const user = createMockUser();
renderWithRouter(
);
const searchInput = screen.getByPlaceholderText('Search...');
expect(searchInput).toHaveClass('focus:outline-none', 'focus:border-brand-500');
});
});
describe('Sandbox Integration', () => {
it('should render SandboxToggle component', () => {
const user = createMockUser();
renderWithRouter(
);
expect(screen.getByTestId('sandbox-toggle')).toBeInTheDocument();
});
it('should pass sandbox state to SandboxToggle', () => {
const user = createMockUser();
mockUseSandbox.mockReturnValue({
isSandbox: true,
sandboxEnabled: true,
toggleSandbox: vi.fn(),
isToggling: false,
});
renderWithRouter(
);
expect(screen.getByText(/Sandbox: On/i)).toBeInTheDocument();
});
it('should handle sandbox toggle being disabled', () => {
const user = createMockUser();
mockUseSandbox.mockReturnValue({
isSandbox: false,
sandboxEnabled: false,
toggleSandbox: vi.fn(),
isToggling: false,
});
renderWithRouter(
);
expect(screen.getByTestId('sandbox-toggle')).toBeInTheDocument();
});
});
describe('Notification Integration', () => {
it('should render NotificationDropdown', () => {
const user = createMockUser();
renderWithRouter(
);
expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument();
});
it('should pass onTicketClick to NotificationDropdown when provided', () => {
const user = createMockUser();
renderWithRouter(
);
expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument();
});
it('should work without onTicketClick prop', () => {
const user = createMockUser();
renderWithRouter(
);
expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument();
});
});
describe('Language Selector Integration', () => {
it('should render LanguageSelector', () => {
const user = createMockUser();
renderWithRouter(
);
expect(screen.getByTestId('language-selector')).toBeInTheDocument();
});
});
describe('Different User Roles', () => {
it('should render for owner role', () => {
const user = createMockUser({ role: 'owner' });
renderWithRouter(
);
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
});
it('should render for manager role', () => {
const user = createMockUser({ role: 'manager' });
renderWithRouter(
);
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
});
it('should render for staff role', () => {
const user = createMockUser({ role: 'staff' });
renderWithRouter(
);
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
});
it('should render for platform roles', () => {
const user = createMockUser({ role: 'platform_manager' });
renderWithRouter(
);
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
});
});
describe('Layout and Styling', () => {
it('should have fixed height', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
const header = container.querySelector('header');
expect(header).toHaveClass('h-16');
});
it('should have border at bottom', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
const header = container.querySelector('header');
expect(header).toHaveClass('border-b');
});
it('should use flexbox layout', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
const header = container.querySelector('header');
expect(header).toHaveClass('flex', 'items-center', 'justify-between');
});
it('should have responsive padding', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
const header = container.querySelector('header');
expect(header).toHaveClass('px-4', 'sm:px-8');
});
});
describe('Accessibility', () => {
it('should have semantic header element', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
expect(container.querySelector('header')).toBeInTheDocument();
});
it('should have proper button roles', () => {
const user = createMockUser();
renderWithRouter(
);
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBeGreaterThan(0);
});
it('should have focus styles on interactive elements', () => {
const user = createMockUser();
renderWithRouter(
);
const menuButton = screen.getByLabelText('Open sidebar');
expect(menuButton).toHaveClass('focus:outline-none', 'focus:ring-2');
});
});
describe('Responsive Behavior', () => {
it('should hide search on mobile', () => {
const user = createMockUser();
const { container } = renderWithRouter(
);
// Search container is a relative div with hidden md:block classes
const searchContainer = container.querySelector('.hidden.md\\:block');
expect(searchContainer).toBeInTheDocument();
});
it('should show menu button only on mobile', () => {
const user = createMockUser();
renderWithRouter(
);
const menuButton = screen.getByLabelText('Open sidebar');
expect(menuButton).toHaveClass('md:hidden');
});
});
});