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>
This commit is contained in:
759
frontend/src/layouts/__tests__/ManagerLayout.test.tsx
Normal file
759
frontend/src/layouts/__tests__/ManagerLayout.test.tsx
Normal file
@@ -0,0 +1,759 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user