import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BrowserRouter, MemoryRouter } from 'react-router-dom'; import PlatformSidebar from '../PlatformSidebar'; import { User } from '../../types'; // Mock the i18next module vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, fallback?: string) => { const translations: Record = { 'nav.platformDashboard': 'Platform Dashboard', 'nav.dashboard': 'Dashboard', 'nav.businesses': 'Businesses', 'nav.users': 'Users', 'nav.support': 'Support', 'nav.staff': 'Staff', 'nav.platformSettings': 'Platform Settings', 'nav.help': 'Help', 'nav.apiDocs': 'API Docs', }; return translations[key] || fallback || key; }, }), })); // Mock the SmoothScheduleLogo component vi.mock('../SmoothScheduleLogo', () => ({ default: ({ className }: { className?: string }) => (
Logo
), })); describe('PlatformSidebar', () => { const mockSuperuser: User = { id: '1', name: 'Super User', email: 'super@example.com', role: 'superuser', }; const mockPlatformManager: User = { id: '2', name: 'Platform Manager', email: 'manager@example.com', role: 'platform_manager', }; const mockPlatformSupport: User = { id: '3', name: 'Platform Support', email: 'support@example.com', role: 'platform_support', }; const mockToggleCollapse = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); describe('Rendering', () => { it('renders the sidebar with logo and user role', () => { render( ); expect(screen.getByTestId('smooth-schedule-logo')).toBeInTheDocument(); expect(screen.getByText('Smooth Schedule')).toBeInTheDocument(); expect(screen.getByText('superuser')).toBeInTheDocument(); }); it('renders all navigation links for superuser', () => { render( ); // Operations section expect(screen.getByText('Operations')).toBeInTheDocument(); expect(screen.getByText('Dashboard')).toBeInTheDocument(); expect(screen.getByText('Businesses')).toBeInTheDocument(); expect(screen.getByText('Users')).toBeInTheDocument(); expect(screen.getByText('Support')).toBeInTheDocument(); expect(screen.getAllByText('Email Addresses')[0]).toBeInTheDocument(); // System section (superuser only) expect(screen.getByText('System')).toBeInTheDocument(); expect(screen.getByText('Staff')).toBeInTheDocument(); expect(screen.getByText('Platform Settings')).toBeInTheDocument(); // Help section expect(screen.getByText('Help')).toBeInTheDocument(); expect(screen.getAllByText('Email Settings')[0]).toBeInTheDocument(); expect(screen.getByText('API Docs')).toBeInTheDocument(); }); it('hides system section for platform manager', () => { render( ); // Operations section visible expect(screen.getByText('Dashboard')).toBeInTheDocument(); expect(screen.getByText('Businesses')).toBeInTheDocument(); // System section not visible expect(screen.queryByText('System')).not.toBeInTheDocument(); expect(screen.queryByText('Staff')).not.toBeInTheDocument(); expect(screen.queryByText('Platform Settings')).not.toBeInTheDocument(); }); it('hides system section and dashboard for platform support', () => { render( ); // Dashboard not visible for support expect(screen.queryByText('Dashboard')).not.toBeInTheDocument(); // Operations section visible expect(screen.getByText('Businesses')).toBeInTheDocument(); expect(screen.getByText('Users')).toBeInTheDocument(); // System section not visible expect(screen.queryByText('System')).not.toBeInTheDocument(); expect(screen.queryByText('Staff')).not.toBeInTheDocument(); }); it('displays role with underscores replaced by spaces', () => { render( ); expect(screen.getByText('platform manager')).toBeInTheDocument(); }); }); describe('Collapsed State', () => { it('hides text labels when collapsed', () => { render( ); // Logo should be visible expect(screen.getByTestId('smooth-schedule-logo')).toBeInTheDocument(); // Text should be hidden expect(screen.queryByText('Smooth Schedule')).not.toBeInTheDocument(); expect(screen.queryByText('superuser')).not.toBeInTheDocument(); // Section headers should show abbreviated versions expect(screen.getByText('Ops')).toBeInTheDocument(); expect(screen.getByText('Sys')).toBeInTheDocument(); }); it('shows full section names when expanded', () => { render( ); expect(screen.getByText('Operations')).toBeInTheDocument(); expect(screen.getByText('System')).toBeInTheDocument(); expect(screen.queryByText('Ops')).not.toBeInTheDocument(); expect(screen.queryByText('Sys')).not.toBeInTheDocument(); }); it('applies correct width classes based on collapsed state', () => { const { container, rerender } = render( ); const sidebar = container.firstChild as HTMLElement; expect(sidebar).toHaveClass('w-64'); expect(sidebar).not.toHaveClass('w-20'); rerender( ); expect(sidebar).toHaveClass('w-20'); expect(sidebar).not.toHaveClass('w-64'); }); }); describe('Toggle Collapse Button', () => { it('calls toggleCollapse when clicked', async () => { const user = userEvent.setup(); render( ); const toggleButton = screen.getByRole('button', { name: /collapse sidebar/i }); await user.click(toggleButton); expect(mockToggleCollapse).toHaveBeenCalledTimes(1); }); it('has correct aria-label when collapsed', () => { render( ); expect(screen.getByRole('button', { name: /expand sidebar/i })).toBeInTheDocument(); }); it('has correct aria-label when expanded', () => { render( ); expect(screen.getByRole('button', { name: /collapse sidebar/i })).toBeInTheDocument(); }); }); describe('Active Link Highlighting', () => { it('highlights the active link based on current path', () => { render( ); const businessesLink = screen.getByRole('link', { name: /businesses/i }); const usersLink = screen.getByRole('link', { name: /^users$/i }); // Active link should have active classes expect(businessesLink).toHaveClass('bg-gray-700', 'text-white'); expect(businessesLink).not.toHaveClass('text-gray-400'); // Inactive link should have inactive classes expect(usersLink).toHaveClass('text-gray-400'); expect(usersLink).not.toHaveClass('bg-gray-700'); }); it('highlights dashboard link when on dashboard route', () => { render( ); const dashboardLink = screen.getByRole('link', { name: /dashboard/i }); expect(dashboardLink).toHaveClass('bg-gray-700', 'text-white'); }); it('highlights link for nested routes', () => { render( ); const businessesLink = screen.getByRole('link', { name: /businesses/i }); expect(businessesLink).toHaveClass('bg-gray-700', 'text-white'); }); it('highlights staff link when on staff route', () => { render( ); const staffLink = screen.getByRole('link', { name: /staff/i }); expect(staffLink).toHaveClass('bg-gray-700', 'text-white'); }); it('highlights help link when on help route', () => { render( ); const apiDocsLink = screen.getByRole('link', { name: /api docs/i }); expect(apiDocsLink).toHaveClass('bg-gray-700', 'text-white'); }); }); describe('Navigation Links', () => { it('has correct href attributes for all links', () => { render( ); expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('href', '/platform/dashboard'); expect(screen.getByRole('link', { name: /businesses/i })).toHaveAttribute('href', '/platform/businesses'); expect(screen.getByRole('link', { name: /^users$/i })).toHaveAttribute('href', '/platform/users'); expect(screen.getByRole('link', { name: /support/i })).toHaveAttribute('href', '/platform/support'); expect(screen.getByRole('link', { name: /staff/i })).toHaveAttribute('href', '/platform/staff'); expect(screen.getByRole('link', { name: /platform settings/i })).toHaveAttribute('href', '/platform/settings'); expect(screen.getByRole('link', { name: /api docs/i })).toHaveAttribute('href', '/help/api'); }); it('shows title attributes on links for accessibility', () => { render( ); expect(screen.getByRole('link', { name: /dashboard/i })).toHaveAttribute('title', 'Platform Dashboard'); expect(screen.getByRole('link', { name: /businesses/i })).toHaveAttribute('title', 'Businesses'); expect(screen.getByRole('link', { name: /^users$/i })).toHaveAttribute('title', 'Users'); }); }); describe('Icons', () => { it('renders lucide-react icons for all navigation items', () => { const { container } = render( ); // Check that SVG icons are present (lucide-react renders as SVG) const svgs = container.querySelectorAll('svg'); // Should have: logo + icons for each nav item expect(svgs.length).toBeGreaterThanOrEqual(10); }); it('keeps icons visible when collapsed', () => { const { container } = render( ); // Icons should still be present when collapsed const svgs = container.querySelectorAll('svg'); expect(svgs.length).toBeGreaterThanOrEqual(10); }); }); describe('Responsive Design', () => { it('applies flex column layout', () => { const { container } = render( ); const sidebar = container.firstChild as HTMLElement; expect(sidebar).toHaveClass('flex', 'flex-col', 'h-full'); }); it('applies dark theme colors', () => { const { container } = render( ); const sidebar = container.firstChild as HTMLElement; expect(sidebar).toHaveClass('bg-gray-900', 'text-white'); }); it('has transition classes for smooth collapse animation', () => { const { container } = render( ); const sidebar = container.firstChild as HTMLElement; expect(sidebar).toHaveClass('transition-all', 'duration-300'); }); }); describe('Role-Based Access Control', () => { it('shows dashboard for superuser and platform_manager only', () => { const { rerender } = render( ); expect(screen.queryByText('Dashboard')).toBeInTheDocument(); rerender( ); expect(screen.queryByText('Dashboard')).toBeInTheDocument(); rerender( ); expect(screen.queryByText('Dashboard')).not.toBeInTheDocument(); }); it('shows system section only for superuser', () => { const { rerender } = render( ); expect(screen.queryByText('System')).toBeInTheDocument(); expect(screen.queryByText('Staff')).toBeInTheDocument(); rerender( ); expect(screen.queryByText('System')).not.toBeInTheDocument(); expect(screen.queryByText('Staff')).not.toBeInTheDocument(); rerender( ); expect(screen.queryByText('System')).not.toBeInTheDocument(); }); it('always shows common operations links for all roles', () => { const roles: User[] = [mockSuperuser, mockPlatformManager, mockPlatformSupport]; roles.forEach((user) => { const { unmount } = render( ); expect(screen.getByText('Businesses')).toBeInTheDocument(); expect(screen.getByText('Users')).toBeInTheDocument(); expect(screen.getByText('Support')).toBeInTheDocument(); unmount(); }); }); }); describe('Accessibility', () => { it('has semantic HTML structure with nav element', () => { const { container } = render( ); const nav = container.querySelector('nav'); expect(nav).toBeInTheDocument(); }); it('provides proper button label for keyboard users', () => { render( ); const button = screen.getByRole('button', { name: /collapse sidebar/i }); expect(button).toHaveAccessibleName(); }); it('all links have accessible names', () => { render( ); const links = screen.getAllByRole('link'); links.forEach((link) => { expect(link).toHaveAccessibleName(); }); }); it('maintains focus visibility for keyboard navigation', () => { const { container } = render( ); const button = screen.getByRole('button', { name: /collapse sidebar/i }); expect(button).toHaveClass('focus:outline-none'); }); }); describe('Edge Cases', () => { it('handles user with empty name gracefully', () => { const userWithoutName: User = { ...mockSuperuser, name: '', }; render( ); // Should still render without crashing expect(screen.getByTestId('smooth-schedule-logo')).toBeInTheDocument(); }); it('handles missing translation gracefully', () => { // Translation mock should return the key if translation is missing render( ); // Should render without errors even with missing translations expect(screen.getByTestId('smooth-schedule-logo')).toBeInTheDocument(); }); it('handles rapid collapse/expand toggling', async () => { const user = userEvent.setup(); render( ); const button = screen.getByRole('button', { name: /collapse sidebar/i }); // Rapidly click multiple times await user.click(button); await user.click(button); await user.click(button); expect(mockToggleCollapse).toHaveBeenCalledTimes(3); }); }); });