import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import ManagerLayout from '../ManagerLayout'; import { User } from '../../types'; // Mock react-router-dom's Outlet vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, Outlet: () =>
Page Content
, }; }); // Mock PlatformSidebar component vi.mock('../../components/PlatformSidebar', () => ({ default: ({ user, isCollapsed, toggleCollapse, onSignOut }: any) => (
{user.name}
{user.role}
), })); // Mock lucide-react icons vi.mock('lucide-react', () => ({ Moon: ({ size }: { size: number }) => , Sun: ({ size }: { size: number }) => , Bell: ({ size }: { size: number }) => , Globe: ({ size }: { size: number }) => , Menu: ({ size }: { size: number }) => , })); // Mock useScrollToTop hook vi.mock('../../hooks/useScrollToTop', () => ({ useScrollToTop: vi.fn(), })); describe('ManagerLayout', () => { const mockToggleTheme = vi.fn(); const mockOnSignOut = vi.fn(); const managerUser: User = { id: '1', name: 'John Manager', email: 'manager@platform.com', role: 'platform_manager', }; const superUser: User = { id: '2', name: 'Admin User', email: 'admin@platform.com', role: 'superuser', }; const supportUser: User = { id: '3', name: 'Support User', email: 'support@platform.com', role: 'platform_support', }; beforeEach(() => { vi.clearAllMocks(); }); const renderLayout = (user: User = managerUser, darkMode: boolean = false) => { return render( ); }; describe('Rendering Children Content', () => { it('renders the main layout structure', () => { renderLayout(); // Check that main container exists const mainContainer = screen.getByRole('main'); expect(mainContainer).toBeInTheDocument(); }); it('renders Outlet for nested routes', () => { renderLayout(); // Check that Outlet content is rendered expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); expect(screen.getByText('Page Content')).toBeInTheDocument(); }); it('renders the header with correct elements', () => { renderLayout(); // Check header exists with proper structure const header = screen.getByRole('banner'); expect(header).toBeInTheDocument(); expect(header).toHaveClass('bg-white', 'dark:bg-gray-800'); }); it('renders main content area with correct styling', () => { renderLayout(); const mainContent = screen.getByRole('main'); expect(mainContent).toHaveClass('flex-1', 'overflow-auto', 'bg-gray-50', 'dark:bg-gray-900'); }); it('applies dark mode classes correctly', () => { renderLayout(managerUser, true); const mainContent = screen.getByRole('main'); expect(mainContent).toHaveClass('dark:bg-gray-900'); }); it('applies light mode classes correctly', () => { renderLayout(managerUser, false); const mainContent = screen.getByRole('main'); expect(mainContent).toHaveClass('bg-gray-50'); }); }); describe('Manager-Specific Navigation', () => { it('renders PlatformSidebar with correct props', () => { renderLayout(); const sidebars = screen.getAllByTestId('platform-sidebar'); expect(sidebars.length).toBe(2); // Mobile and desktop // Check user data is passed correctly (using first sidebar) const userElements = screen.getAllByTestId('sidebar-user'); expect(userElements[0]).toHaveTextContent('John Manager'); const roleElements = screen.getAllByTestId('sidebar-role'); expect(roleElements[0]).toHaveTextContent('platform_manager'); }); it('displays Management Console in breadcrumb', () => { renderLayout(); expect(screen.getByText('Management Console')).toBeInTheDocument(); }); it('displays domain information in breadcrumb', () => { renderLayout(); expect(screen.getByText('smoothschedule.com')).toBeInTheDocument(); }); it('renders globe icon in breadcrumb', () => { renderLayout(); const globeIcon = screen.getByTestId('globe-icon'); expect(globeIcon).toBeInTheDocument(); expect(globeIcon).toHaveAttribute('width', '16'); expect(globeIcon).toHaveAttribute('height', '16'); }); it('hides breadcrumb on mobile', () => { renderLayout(); const breadcrumb = screen.getByText('Management Console').closest('div'); expect(breadcrumb).toHaveClass('hidden', 'md:flex'); }); it('handles sidebar collapse state', () => { renderLayout(); const collapseButton = screen.getByTestId('sidebar-collapse'); expect(collapseButton).toHaveTextContent('Collapse'); // Click to collapse fireEvent.click(collapseButton); // Note: The sidebar is mocked, so we just verify the button exists expect(collapseButton).toBeInTheDocument(); }); it('renders desktop sidebar by default', () => { renderLayout(); const sidebar = screen.getByTestId('platform-sidebar'); const desktopSidebar = sidebar.closest('.md\\:flex'); expect(desktopSidebar).toBeInTheDocument(); }); it('mobile sidebar is hidden by default', () => { renderLayout(); // Mobile menu should be off-screen initially const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('-translate-x-full'); }); it('mobile menu button opens mobile sidebar', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); // After clicking, mobile sidebar should be visible const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('translate-x-0'); }); it('clicking backdrop closes mobile menu', () => { renderLayout(); // Open mobile menu const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); // Find and click backdrop const backdrop = document.querySelector('.bg-black\\/50'); expect(backdrop).toBeInTheDocument(); fireEvent.click(backdrop!); // Mobile sidebar should be hidden again const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('-translate-x-full'); }); }); describe('Access Controls', () => { it('allows platform_manager role to access layout', () => { renderLayout(managerUser); expect(screen.getByTestId('sidebar-role')).toHaveTextContent('platform_manager'); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); it('allows superuser role to access layout', () => { renderLayout(superUser); expect(screen.getByTestId('sidebar-role')).toHaveTextContent('superuser'); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); it('allows platform_support role to access layout', () => { renderLayout(supportUser); expect(screen.getByTestId('sidebar-role')).toHaveTextContent('platform_support'); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); it('renders sign out button for authenticated users', () => { renderLayout(); const signOutButton = screen.getByTestId('sidebar-signout'); expect(signOutButton).toBeInTheDocument(); }); it('calls onSignOut when sign out button is clicked', () => { renderLayout(); const signOutButton = screen.getByTestId('sidebar-signout'); fireEvent.click(signOutButton); expect(mockOnSignOut).toHaveBeenCalledTimes(1); }); it('renders layout for different user emails', () => { const customUser: User = { ...managerUser, email: 'custom@example.com', }; renderLayout(customUser); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); it('renders layout for users with numeric IDs', () => { const numericIdUser: User = { ...managerUser, id: 123, }; renderLayout(numericIdUser); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); }); describe('Theme Toggle', () => { it('renders theme toggle button', () => { renderLayout(); const themeButton = screen.getByRole('button', { name: '' }).parentElement?.querySelector('button'); expect(themeButton).toBeInTheDocument(); }); it('displays Moon icon in light mode', () => { renderLayout(managerUser, false); const moonIcon = screen.getByTestId('moon-icon'); expect(moonIcon).toBeInTheDocument(); expect(screen.queryByTestId('sun-icon')).not.toBeInTheDocument(); }); it('displays Sun icon in dark mode', () => { renderLayout(managerUser, true); const sunIcon = screen.getByTestId('sun-icon'); expect(sunIcon).toBeInTheDocument(); expect(screen.queryByTestId('moon-icon')).not.toBeInTheDocument(); }); it('calls toggleTheme when theme button is clicked', () => { renderLayout(); // Find the button containing the moon icon const moonIcon = screen.getByTestId('moon-icon'); const themeButton = moonIcon.closest('button'); expect(themeButton).toBeInTheDocument(); fireEvent.click(themeButton!); expect(mockToggleTheme).toHaveBeenCalledTimes(1); }); it('theme button has proper styling', () => { renderLayout(); const moonIcon = screen.getByTestId('moon-icon'); const themeButton = moonIcon.closest('button'); expect(themeButton).toHaveClass('text-gray-400', 'hover:text-gray-600'); }); it('icon size is correct', () => { renderLayout(); const moonIcon = screen.getByTestId('moon-icon'); expect(moonIcon).toHaveAttribute('width', '20'); expect(moonIcon).toHaveAttribute('height', '20'); }); }); describe('Notification Bell', () => { it('renders notification bell icon', () => { renderLayout(); const bellIcon = screen.getByTestId('bell-icon'); expect(bellIcon).toBeInTheDocument(); }); it('bell icon has correct size', () => { renderLayout(); const bellIcon = screen.getByTestId('bell-icon'); expect(bellIcon).toHaveAttribute('width', '20'); expect(bellIcon).toHaveAttribute('height', '20'); }); it('bell button has proper styling', () => { renderLayout(); const bellIcon = screen.getByTestId('bell-icon'); const bellButton = bellIcon.closest('button'); expect(bellButton).toHaveClass('text-gray-400', 'hover:text-gray-600'); }); it('bell button is clickable', () => { renderLayout(); const bellIcon = screen.getByTestId('bell-icon'); const bellButton = bellIcon.closest('button'); expect(bellButton).toBeInTheDocument(); fireEvent.click(bellButton!); // Button should be clickable (no error thrown) }); }); describe('Mobile Menu', () => { it('renders mobile menu button with Menu icon', () => { renderLayout(); const menuIcon = screen.getByTestId('menu-icon'); expect(menuIcon).toBeInTheDocument(); }); it('mobile menu button has correct aria-label', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toBeInTheDocument(); }); it('mobile menu button is only visible on mobile', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveClass('md:hidden'); }); it('menu icon has correct size', () => { renderLayout(); const menuIcon = screen.getByTestId('menu-icon'); expect(menuIcon).toHaveAttribute('width', '24'); expect(menuIcon).toHaveAttribute('height', '24'); }); it('toggles mobile menu visibility', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); // Initially closed let mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; let mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('-translate-x-full'); // Open menu fireEvent.click(menuButton); mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('translate-x-0'); }); it('mobile backdrop appears when menu is open', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); // No backdrop initially expect(document.querySelector('.bg-black\\/50')).not.toBeInTheDocument(); // Open menu fireEvent.click(menuButton); // Backdrop should appear expect(document.querySelector('.bg-black\\/50')).toBeInTheDocument(); }); it('mobile backdrop has correct z-index', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); const backdrop = document.querySelector('.bg-black\\/50'); expect(backdrop).toHaveClass('z-30'); }); it('mobile sidebar has higher z-index than backdrop', () => { renderLayout(); const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('z-40'); }); }); describe('Layout Responsiveness', () => { it('applies responsive padding to header', () => { renderLayout(); const header = screen.getByRole('banner'); expect(header).toHaveClass('px-4', 'sm:px-8'); }); it('main content has proper spacing', () => { renderLayout(); const mainContent = screen.getByRole('main'); expect(mainContent).toHaveClass('p-8'); }); it('desktop sidebar is hidden on mobile', () => { renderLayout(); const desktopSidebar = screen.getAllByTestId('platform-sidebar')[1].closest('.md\\:flex'); expect(desktopSidebar).toHaveClass('hidden'); }); it('layout uses flexbox for proper structure', () => { renderLayout(); const container = screen.getByRole('main').closest('.flex'); expect(container).toHaveClass('flex', 'h-full'); }); it('main content area is scrollable', () => { renderLayout(); const mainContent = screen.getByRole('main'); expect(mainContent).toHaveClass('overflow-auto'); }); it('layout has proper height constraints', () => { renderLayout(); const container = screen.getByRole('main').closest('.flex'); expect(container).toHaveClass('h-full'); }); }); describe('Styling and Visual State', () => { it('applies background color classes', () => { renderLayout(); const container = screen.getByRole('main').closest('.flex'); expect(container).toHaveClass('bg-gray-100', 'dark:bg-gray-900'); }); it('header has border', () => { renderLayout(); const header = screen.getByRole('banner'); expect(header).toHaveClass('border-b', 'border-gray-200', 'dark:border-gray-700'); }); it('header has fixed height', () => { renderLayout(); const header = screen.getByRole('banner'); expect(header).toHaveClass('h-16'); }); it('applies transition classes for animations', () => { renderLayout(); const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('transition-transform', 'duration-300', 'ease-in-out'); }); it('buttons have hover states', () => { renderLayout(); const moonIcon = screen.getByTestId('moon-icon'); const themeButton = moonIcon.closest('button'); expect(themeButton).toHaveClass('hover:text-gray-600'); }); it('menu button has negative margin for alignment', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveClass('-ml-2'); }); }); describe('Scroll Behavior', () => { it('calls useScrollToTop hook on mount', () => { const { useScrollToTop } = require('../../hooks/useScrollToTop'); renderLayout(); expect(useScrollToTop).toHaveBeenCalled(); }); it('passes main content ref to useScrollToTop', () => { const { useScrollToTop } = require('../../hooks/useScrollToTop'); renderLayout(); // Verify hook was called with a ref expect(useScrollToTop).toHaveBeenCalledWith(expect.objectContaining({ current: expect.any(Object), })); }); }); describe('Edge Cases', () => { it('handles user without optional fields', () => { const minimalUser: User = { id: '1', name: 'Test User', email: 'test@example.com', role: 'platform_manager', }; renderLayout(minimalUser); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); it('renders with extremely long user names', () => { const longNameUser: User = { ...managerUser, name: 'This Is An Extremely Long User Name That Should Still Render Properly Without Breaking The Layout', }; renderLayout(longNameUser); expect(screen.getByTestId('sidebar-user')).toBeInTheDocument(); }); it('handles rapid theme toggle clicks', () => { renderLayout(); const moonIcon = screen.getByTestId('moon-icon'); const themeButton = moonIcon.closest('button'); fireEvent.click(themeButton!); fireEvent.click(themeButton!); fireEvent.click(themeButton!); expect(mockToggleTheme).toHaveBeenCalledTimes(3); }); it('handles rapid mobile menu toggles', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); fireEvent.click(menuButton); fireEvent.click(menuButton); // Should not crash expect(menuButton).toBeInTheDocument(); }); it('maintains state during re-renders', () => { const { rerender } = renderLayout(); // Open mobile menu const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); // Re-render with same props rerender( ); // State should persist const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('translate-x-0'); }); }); describe('Accessibility', () => { it('header has correct semantic role', () => { renderLayout(); const header = screen.getByRole('banner'); expect(header.tagName).toBe('HEADER'); }); it('main has correct semantic role', () => { renderLayout(); const main = screen.getByRole('main'); expect(main.tagName).toBe('MAIN'); }); it('buttons have proper interactive elements', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton.tagName).toBe('BUTTON'); }); it('mobile menu button has aria-label', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveAttribute('aria-label', 'Open sidebar'); }); it('all interactive elements are keyboard accessible', () => { renderLayout(); const menuButton = screen.getByLabelText('Open sidebar'); const moonIcon = screen.getByTestId('moon-icon'); const themeButton = moonIcon.closest('button'); const bellIcon = screen.getByTestId('bell-icon'); const bellButton = bellIcon.closest('button'); expect(menuButton.tagName).toBe('BUTTON'); expect(themeButton?.tagName).toBe('BUTTON'); expect(bellButton?.tagName).toBe('BUTTON'); }); }); describe('Component Integration', () => { it('renders without crashing', () => { expect(() => renderLayout()).not.toThrow(); }); it('renders all major sections together', () => { renderLayout(); expect(screen.getByTestId('platform-sidebar')).toBeInTheDocument(); expect(screen.getByRole('banner')).toBeInTheDocument(); expect(screen.getByRole('main')).toBeInTheDocument(); expect(screen.getByTestId('outlet-content')).toBeInTheDocument(); }); it('passes correct props to PlatformSidebar', () => { renderLayout(); expect(screen.getByTestId('sidebar-user')).toHaveTextContent('John Manager'); expect(screen.getByTestId('sidebar-signout')).toBeInTheDocument(); }); it('integrates with React Router Outlet', () => { renderLayout(); const outlet = screen.getByTestId('outlet-content'); expect(outlet).toHaveTextContent('Page Content'); }); it('handles multiple simultaneous interactions', () => { renderLayout(); // Open mobile menu const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); // Toggle theme const moonIcon = screen.getByTestId('moon-icon'); const themeButton = moonIcon.closest('button'); fireEvent.click(themeButton!); // Click bell const bellIcon = screen.getByTestId('bell-icon'); const bellButton = bellIcon.closest('button'); fireEvent.click(bellButton!); expect(mockToggleTheme).toHaveBeenCalledTimes(1); const mobileSidebar = screen.getAllByTestId('platform-sidebar')[0]; const mobileContainer = mobileSidebar.closest('.fixed'); expect(mobileContainer).toHaveClass('translate-x-0'); }); }); });