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:
657
frontend/src/layouts/__tests__/PlatformLayout.test.tsx
Normal file
657
frontend/src/layouts/__tests__/PlatformLayout.test.tsx
Normal file
@@ -0,0 +1,657 @@
|
||||
/**
|
||||
* Unit tests for PlatformLayout component
|
||||
*
|
||||
* Tests all layout functionality including:
|
||||
* - Rendering children content via Outlet
|
||||
* - Platform admin navigation (sidebar, mobile menu)
|
||||
* - User info displays (UserProfileDropdown, theme toggle, language selector)
|
||||
* - Notification dropdown
|
||||
* - Ticket modal functionality
|
||||
* - Mobile menu behavior
|
||||
* - Floating help button
|
||||
* - Scroll to top on route change
|
||||
* - Conditional padding for special routes
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import PlatformLayout from '../PlatformLayout';
|
||||
import { User } from '../../types';
|
||||
|
||||
// Mock child components
|
||||
vi.mock('../../components/PlatformSidebar', () => ({
|
||||
default: ({ user, isCollapsed, toggleCollapse }: any) => (
|
||||
<div data-testid="platform-sidebar">
|
||||
<div data-testid="sidebar-user">{user.name}</div>
|
||||
<div data-testid="sidebar-collapsed">{isCollapsed.toString()}</div>
|
||||
<button onClick={toggleCollapse} data-testid="toggle-collapse">Toggle</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/UserProfileDropdown', () => ({
|
||||
default: ({ user }: any) => (
|
||||
<div data-testid="user-profile-dropdown">
|
||||
<div data-testid="profile-user-name">{user.name}</div>
|
||||
<div data-testid="profile-user-email">{user.email}</div>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/NotificationDropdown', () => ({
|
||||
default: ({ onTicketClick }: any) => (
|
||||
<div data-testid="notification-dropdown">
|
||||
<button onClick={() => onTicketClick('ticket-123')} data-testid="notification-ticket-btn">
|
||||
Open Ticket
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/LanguageSelector', () => ({
|
||||
default: () => <div data-testid="language-selector">Language Selector</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../../components/TicketModal', () => ({
|
||||
default: ({ ticket, onClose }: any) => (
|
||||
<div data-testid="ticket-modal">
|
||||
<div data-testid="ticket-id">{ticket.id}</div>
|
||||
<button onClick={onClose} data-testid="close-ticket-modal">Close</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../components/FloatingHelpButton', () => ({
|
||||
default: () => <div data-testid="floating-help-button">Help</div>,
|
||||
}));
|
||||
|
||||
// Mock hooks
|
||||
vi.mock('../../hooks/useTickets', () => ({
|
||||
useTicket: vi.fn((ticketId) => {
|
||||
if (ticketId === 'ticket-123') {
|
||||
return {
|
||||
data: {
|
||||
id: 'ticket-123',
|
||||
subject: 'Test Ticket',
|
||||
description: 'Test description',
|
||||
status: 'OPEN',
|
||||
priority: 'MEDIUM',
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return { data: null, isLoading: false, error: null };
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/useScrollToTop', () => ({
|
||||
useScrollToTop: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock react-router-dom 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>,
|
||||
useLocation: vi.fn(() => ({ pathname: '/' })),
|
||||
};
|
||||
});
|
||||
|
||||
// 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} />,
|
||||
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} />,
|
||||
}));
|
||||
|
||||
describe('PlatformLayout', () => {
|
||||
const mockUser: User = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john@platform.com',
|
||||
role: 'superuser',
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
user: mockUser,
|
||||
darkMode: false,
|
||||
toggleTheme: vi.fn(),
|
||||
onSignOut: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderLayout = (props = {}) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} {...props} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the layout with all main components', () => {
|
||||
renderLayout();
|
||||
|
||||
// Check for main structural elements (there are 2 sidebars: mobile and desktop)
|
||||
expect(screen.getAllByTestId('platform-sidebar')).toHaveLength(2);
|
||||
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('language-selector')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('floating-help-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render children content via Outlet', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.getByTestId('outlet-content')).toBeInTheDocument();
|
||||
expect(screen.getByText('Page Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the platform header with branding', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.getByText('smoothschedule.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('Admin Console')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('globe-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render desktop sidebar (hidden on mobile)', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const desktopSidebar = container.querySelector('.hidden.md\\:flex.md\\:flex-shrink-0');
|
||||
expect(desktopSidebar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render mobile menu button', () => {
|
||||
renderLayout();
|
||||
|
||||
const menuButton = screen.getByLabelText('Open sidebar');
|
||||
expect(menuButton).toBeInTheDocument();
|
||||
expect(screen.getByTestId('menu-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Info Display', () => {
|
||||
it('should display user info in UserProfileDropdown', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.getByTestId('profile-user-name')).toHaveTextContent('John Doe');
|
||||
expect(screen.getByTestId('profile-user-email')).toHaveTextContent('john@platform.com');
|
||||
});
|
||||
|
||||
it('should display user in sidebar', () => {
|
||||
renderLayout();
|
||||
|
||||
// Both mobile and desktop sidebars show user
|
||||
const sidebarUsers = screen.getAllByTestId('sidebar-user');
|
||||
expect(sidebarUsers).toHaveLength(2);
|
||||
sidebarUsers.forEach(el => expect(el).toHaveTextContent('John Doe'));
|
||||
});
|
||||
|
||||
it('should handle different user roles', () => {
|
||||
const managerUser: User = {
|
||||
id: '2',
|
||||
name: 'Jane Manager',
|
||||
email: 'jane@platform.com',
|
||||
role: 'platform_manager',
|
||||
};
|
||||
|
||||
renderLayout({ user: managerUser });
|
||||
|
||||
expect(screen.getByTestId('profile-user-name')).toHaveTextContent('Jane Manager');
|
||||
expect(screen.getByTestId('profile-user-email')).toHaveTextContent('jane@platform.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Theme Toggle', () => {
|
||||
it('should show Moon icon when in light mode', () => {
|
||||
renderLayout({ darkMode: false });
|
||||
|
||||
expect(screen.getByTestId('moon-icon')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('sun-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Sun icon when in dark mode', () => {
|
||||
renderLayout({ darkMode: true });
|
||||
|
||||
expect(screen.getByTestId('sun-icon')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('moon-icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call toggleTheme when theme button is clicked', () => {
|
||||
const toggleTheme = vi.fn();
|
||||
renderLayout({ toggleTheme });
|
||||
|
||||
const themeButton = screen.getByTestId('moon-icon').closest('button');
|
||||
expect(themeButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(themeButton!);
|
||||
expect(toggleTheme).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should toggle between light and dark mode icons', () => {
|
||||
const { rerender } = render(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} darkMode={false} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('moon-icon')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} darkMode={true} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('sun-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mobile Menu', () => {
|
||||
it('should not show mobile menu by default', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const mobileMenu = container.querySelector('.fixed.inset-y-0.left-0.z-40');
|
||||
expect(mobileMenu).toHaveClass('-translate-x-full');
|
||||
});
|
||||
|
||||
it('should open mobile menu when menu button is clicked', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const menuButton = screen.getByLabelText('Open sidebar');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const mobileMenu = container.querySelector('.fixed.inset-y-0.left-0.z-40');
|
||||
expect(mobileMenu).toHaveClass('translate-x-0');
|
||||
});
|
||||
|
||||
it('should show backdrop when mobile menu is open', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
// Initially no backdrop
|
||||
let backdrop = container.querySelector('.fixed.inset-0.z-30.bg-black\\/50');
|
||||
expect(backdrop).not.toBeInTheDocument();
|
||||
|
||||
// Open menu
|
||||
const menuButton = screen.getByLabelText('Open sidebar');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
// Backdrop should appear
|
||||
backdrop = container.querySelector('.fixed.inset-0.z-30.bg-black\\/50');
|
||||
expect(backdrop).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close mobile menu when backdrop is clicked', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
// Open menu
|
||||
const menuButton = screen.getByLabelText('Open sidebar');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
// Verify menu is open
|
||||
let mobileMenu = container.querySelector('.fixed.inset-y-0.left-0.z-40');
|
||||
expect(mobileMenu).toHaveClass('translate-x-0');
|
||||
|
||||
// Click backdrop
|
||||
const backdrop = container.querySelector('.fixed.inset-0.z-30.bg-black\\/50');
|
||||
fireEvent.click(backdrop!);
|
||||
|
||||
// Menu should be closed
|
||||
mobileMenu = container.querySelector('.fixed.inset-y-0.left-0.z-40');
|
||||
expect(mobileMenu).toHaveClass('-translate-x-full');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sidebar Collapse', () => {
|
||||
it('should toggle sidebar collapse state', () => {
|
||||
renderLayout();
|
||||
|
||||
// Initially not collapsed (both mobile and desktop)
|
||||
const collapsedStates = screen.getAllByTestId('sidebar-collapsed');
|
||||
expect(collapsedStates).toHaveLength(2);
|
||||
collapsedStates.forEach(el => expect(el).toHaveTextContent('false'));
|
||||
|
||||
// Click toggle button
|
||||
const toggleButtons = screen.getAllByTestId('toggle-collapse');
|
||||
expect(toggleButtons.length).toBeGreaterThan(0);
|
||||
fireEvent.click(toggleButtons[1]); // Desktop sidebar
|
||||
|
||||
// Verify button exists and can be clicked
|
||||
expect(toggleButtons[1]).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ticket Modal', () => {
|
||||
it('should not show ticket modal by default', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.queryByTestId('ticket-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open ticket modal when notification is clicked', async () => {
|
||||
renderLayout();
|
||||
|
||||
const notificationButton = screen.getByTestId('notification-ticket-btn');
|
||||
fireEvent.click(notificationButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('ticket-modal')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ticket-id')).toHaveTextContent('ticket-123');
|
||||
});
|
||||
});
|
||||
|
||||
it('should close ticket modal when close button is clicked', async () => {
|
||||
renderLayout();
|
||||
|
||||
// Open modal
|
||||
const notificationButton = screen.getByTestId('notification-ticket-btn');
|
||||
fireEvent.click(notificationButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('ticket-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Close modal
|
||||
const closeButton = screen.getByTestId('close-ticket-modal');
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('ticket-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render modal if ticket data is not available', () => {
|
||||
const { useTicket } = require('../../hooks/useTickets');
|
||||
useTicket.mockReturnValue({ data: null, isLoading: false, error: null });
|
||||
|
||||
renderLayout();
|
||||
|
||||
const notificationButton = screen.getByTestId('notification-ticket-btn');
|
||||
fireEvent.click(notificationButton);
|
||||
|
||||
expect(screen.queryByTestId('ticket-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation Components', () => {
|
||||
it('should render all navigation components', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.getByTestId('platform-sidebar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('language-selector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render floating help button', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.getByTestId('floating-help-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Route-specific Padding', () => {
|
||||
it('should apply padding to main content by default', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const mainContent = container.querySelector('main');
|
||||
expect(mainContent).toHaveClass('p-8');
|
||||
});
|
||||
|
||||
it('should not apply padding for API docs route', () => {
|
||||
const mockUseLocation = useLocation as any;
|
||||
mockUseLocation.mockReturnValue({ pathname: '/help/api-docs' });
|
||||
|
||||
const { container } = render(
|
||||
<MemoryRouter initialEntries={['/help/api-docs']}>
|
||||
<PlatformLayout {...defaultProps} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const mainContent = container.querySelector('main');
|
||||
expect(mainContent).not.toHaveClass('p-8');
|
||||
});
|
||||
|
||||
it('should apply padding for other routes', () => {
|
||||
const mockUseLocation = useLocation as any;
|
||||
mockUseLocation.mockReturnValue({ pathname: '/platform/dashboard' });
|
||||
|
||||
const { container } = render(
|
||||
<MemoryRouter initialEntries={['/platform/dashboard']}>
|
||||
<PlatformLayout {...defaultProps} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const mainContent = container.querySelector('main');
|
||||
expect(mainContent).toHaveClass('p-8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper ARIA label for mobile menu button', () => {
|
||||
renderLayout();
|
||||
|
||||
const menuButton = screen.getByLabelText('Open sidebar');
|
||||
expect(menuButton).toHaveAttribute('aria-label', 'Open sidebar');
|
||||
});
|
||||
|
||||
it('should have semantic main element', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const mainElement = container.querySelector('main');
|
||||
expect(mainElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have semantic header element', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const headerElement = container.querySelector('header');
|
||||
expect(headerElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have proper structure for navigation', () => {
|
||||
renderLayout();
|
||||
|
||||
expect(screen.getByTestId('platform-sidebar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layout Structure', () => {
|
||||
it('should have correct flex layout classes', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const mainContainer = container.querySelector('.flex.h-screen.bg-gray-100');
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have scrollable main content area', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const mainContent = container.querySelector('main');
|
||||
expect(mainContent).toHaveClass('flex-1', 'overflow-auto');
|
||||
});
|
||||
|
||||
it('should have fixed height header', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const header = container.querySelector('header');
|
||||
expect(header).toHaveClass('h-16');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Responsive Design', () => {
|
||||
it('should hide branding text on mobile', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const brandingContainer = screen.getByText('smoothschedule.com').parentElement;
|
||||
expect(brandingContainer).toHaveClass('hidden', 'md:flex');
|
||||
});
|
||||
|
||||
it('should show mobile menu button only on mobile', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const menuButton = screen.getByLabelText('Open sidebar').parentElement;
|
||||
expect(menuButton).toHaveClass('md:hidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dark Mode Styling', () => {
|
||||
it('should apply dark mode classes when darkMode is true', () => {
|
||||
const { container } = renderLayout({ darkMode: true });
|
||||
|
||||
const mainContainer = container.querySelector('.dark\\:bg-gray-900');
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have light mode classes by default', () => {
|
||||
const { container } = renderLayout({ darkMode: false });
|
||||
|
||||
const header = container.querySelector('header');
|
||||
expect(header).toHaveClass('bg-white');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should handle complete user flow: open menu, view ticket, close all', async () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
// 1. Open mobile menu
|
||||
const menuButton = screen.getByLabelText('Open sidebar');
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
let mobileMenu = container.querySelector('.fixed.inset-y-0.left-0.z-40');
|
||||
expect(mobileMenu).toHaveClass('translate-x-0');
|
||||
|
||||
// 2. Close mobile menu via backdrop
|
||||
const backdrop = container.querySelector('.fixed.inset-0.z-30.bg-black\\/50');
|
||||
fireEvent.click(backdrop!);
|
||||
|
||||
mobileMenu = container.querySelector('.fixed.inset-y-0.left-0.z-40');
|
||||
expect(mobileMenu).toHaveClass('-translate-x-full');
|
||||
|
||||
// 3. Open ticket modal
|
||||
const notificationButton = screen.getByTestId('notification-ticket-btn');
|
||||
fireEvent.click(notificationButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('ticket-modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// 4. Close ticket modal
|
||||
const closeTicketButton = screen.getByTestId('close-ticket-modal');
|
||||
fireEvent.click(closeTicketButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('ticket-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle theme and update icon', () => {
|
||||
const toggleTheme = vi.fn();
|
||||
const { rerender } = render(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} darkMode={false} toggleTheme={toggleTheme} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Light mode - Moon icon
|
||||
expect(screen.getByTestId('moon-icon')).toBeInTheDocument();
|
||||
|
||||
// Click toggle
|
||||
const themeButton = screen.getByTestId('moon-icon').closest('button');
|
||||
fireEvent.click(themeButton!);
|
||||
expect(toggleTheme).toHaveBeenCalled();
|
||||
|
||||
// Simulate parent state change to dark mode
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} darkMode={true} toggleTheme={toggleTheme} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Dark mode - Sun icon
|
||||
expect(screen.getByTestId('sun-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle user with minimal data', () => {
|
||||
const minimalUser: User = {
|
||||
id: '999',
|
||||
name: 'Test',
|
||||
email: 'test@test.com',
|
||||
role: 'platform_support',
|
||||
};
|
||||
|
||||
renderLayout({ user: minimalUser });
|
||||
|
||||
expect(screen.getByTestId('profile-user-name')).toHaveTextContent('Test');
|
||||
expect(screen.getByTestId('profile-user-email')).toHaveTextContent('test@test.com');
|
||||
});
|
||||
|
||||
it('should handle undefined ticket ID gracefully', async () => {
|
||||
const { useTicket } = require('../../hooks/useTickets');
|
||||
useTicket.mockImplementation((ticketId: any) => {
|
||||
if (!ticketId || ticketId === 'undefined') {
|
||||
return { data: null, isLoading: false, error: null };
|
||||
}
|
||||
return { data: { id: ticketId }, isLoading: false, error: null };
|
||||
});
|
||||
|
||||
renderLayout();
|
||||
|
||||
// Modal should not appear for undefined ticket
|
||||
expect(screen.queryByTestId('ticket-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle rapid state changes', () => {
|
||||
const { container, rerender } = render(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} darkMode={false} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Toggle dark mode multiple times
|
||||
for (let i = 0; i < 5; i++) {
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<PlatformLayout {...defaultProps} darkMode={i % 2 === 0} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// Should still render correctly
|
||||
expect(screen.getByTestId('platform-sidebar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('outlet-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle all platform roles', () => {
|
||||
const roles: Array<User['role']> = ['superuser', 'platform_manager', 'platform_support'];
|
||||
|
||||
roles.forEach((role) => {
|
||||
const roleUser: User = {
|
||||
id: `user-${role}`,
|
||||
name: `${role} User`,
|
||||
email: `${role}@platform.com`,
|
||||
role,
|
||||
};
|
||||
|
||||
const { unmount } = renderLayout({ user: roleUser });
|
||||
expect(screen.getByTestId('profile-user-name')).toHaveTextContent(`${role} User`);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user