Files
smoothschedule/frontend/src/layouts/__tests__/ManagerLayout.test.tsx
poduck 8dc2248f1f feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 02:36:46 -05:00

760 lines
23 KiB
TypeScript

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: () => <div data-testid="outlet-content">Page Content</div>,
};
});
// Mock PlatformSidebar component
vi.mock('../../components/PlatformSidebar', () => ({
default: ({ user, isCollapsed, toggleCollapse, onSignOut }: any) => (
<div data-testid="platform-sidebar">
<div data-testid="sidebar-user">{user.name}</div>
<div data-testid="sidebar-role">{user.role}</div>
<button onClick={toggleCollapse} data-testid="sidebar-collapse">
{isCollapsed ? 'Expand' : 'Collapse'}
</button>
<button onClick={onSignOut} data-testid="sidebar-signout">
Sign Out
</button>
</div>
),
}));
// Mock lucide-react icons
vi.mock('lucide-react', () => ({
Moon: ({ size }: { size: number }) => <svg data-testid="moon-icon" width={size} height={size} />,
Sun: ({ size }: { size: number }) => <svg data-testid="sun-icon" width={size} height={size} />,
Bell: ({ size }: { size: number }) => <svg data-testid="bell-icon" width={size} height={size} />,
Globe: ({ size }: { size: number }) => <svg data-testid="globe-icon" width={size} height={size} />,
Menu: ({ size }: { size: number }) => <svg data-testid="menu-icon" width={size} height={size} />,
}));
// 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(
<MemoryRouter>
<ManagerLayout
user={user}
darkMode={darkMode}
toggleTheme={mockToggleTheme}
onSignOut={mockOnSignOut}
/>
</MemoryRouter>
);
};
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(
<MemoryRouter>
<ManagerLayout
user={managerUser}
darkMode={false}
toggleTheme={mockToggleTheme}
onSignOut={mockOnSignOut}
/>
</MemoryRouter>
);
// 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');
});
});
});