import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, within } from '@testing-library/react'; import React from 'react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import SettingsLayout from '../SettingsLayout'; import { Business, User, PlanPermissions } from '../../types'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, fallback?: string) => { const translations: Record = { 'settings.backToApp': 'Back to App', 'settings.title': 'Settings', 'settings.sections.business': 'Business', 'settings.sections.branding': 'Branding', 'settings.sections.integrations': 'Integrations', 'settings.sections.access': 'Access', 'settings.sections.communication': 'Communication', 'settings.sections.billing': 'Billing', 'settings.general.title': 'General', 'settings.general.description': 'Name, timezone, contact', 'settings.resourceTypes.title': 'Resource Types', 'settings.resourceTypes.description': 'Staff, rooms, equipment', 'settings.booking.title': 'Booking', 'settings.booking.description': 'Booking URL, redirects', 'settings.appearance.title': 'Appearance', 'settings.appearance.description': 'Logo, colors, theme', 'settings.emailTemplates.title': 'Email Templates', 'settings.emailTemplates.description': 'Customize email designs', 'settings.customDomains.title': 'Custom Domains', 'settings.customDomains.description': 'Use your own domain', 'settings.api.title': 'API & Webhooks', 'settings.api.description': 'API tokens, webhooks', 'settings.authentication.title': 'Authentication', 'settings.authentication.description': 'OAuth, social login', 'settings.email.title': 'Email Setup', 'settings.email.description': 'Email addresses for tickets', 'settings.smsCalling.title': 'SMS & Calling', 'settings.smsCalling.description': 'Credits, phone numbers', 'settings.billing.title': 'Plan & Billing', 'settings.billing.description': 'Subscription, invoices', 'settings.quota.title': 'Quota Management', 'settings.quota.description': 'Usage limits, archiving', }; return translations[key] || fallback || key; }, }), })); // Mock lucide-react icons vi.mock('lucide-react', () => ({ ArrowLeft: ({ size }: { size: number }) => , Building2: ({ size }: { size: number }) => , Palette: ({ size }: { size: number }) => , Layers: ({ size }: { size: number }) => , Globe: ({ size }: { size: number }) => , Key: ({ size }: { size: number }) => , Lock: ({ size }: { size: number }) => , Mail: ({ size }: { size: number }) => , Phone: ({ size }: { size: number }) => , CreditCard: ({ size }: { size: number }) => , AlertTriangle: ({ size }: { size: number }) => , Calendar: ({ size }: { size: number }) => , })); // Mock usePlanFeatures hook const mockCanUse = vi.fn(); vi.mock('../../hooks/usePlanFeatures', () => ({ usePlanFeatures: () => ({ canUse: mockCanUse, }), })); // Mock useOutletContext to provide parent context const mockUseOutletContext = vi.fn(); vi.mock('react-router-dom', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useOutletContext: () => mockUseOutletContext(), }; }); describe('SettingsLayout', () => { const mockUser: User = { id: '1', name: 'John Doe', email: 'john@example.com', role: 'owner', }; const mockBusiness: Business = { id: 'business-1', name: 'Test Business', subdomain: 'testbiz', primaryColor: '#3B82F6', secondaryColor: '#10B981', whitelabelEnabled: false, plan: 'Professional', paymentsEnabled: false, requirePaymentMethodToBook: false, cancellationWindowHours: 24, lateCancellationFeePercent: 0, }; const mockUpdateBusiness = vi.fn(); const mockOutletContext = { user: mockUser, business: mockBusiness, updateBusiness: mockUpdateBusiness, }; beforeEach(() => { vi.clearAllMocks(); // Default: all features are unlocked mockCanUse.mockReturnValue(true); // Default: provide parent context mockUseOutletContext.mockReturnValue(mockOutletContext); }); const renderWithRouter = (initialPath = '/settings/general') => { return render( }> General Settings Content} /> Branding Settings Content} /> API Settings Content} /> Billing Settings Content} /> Home Page} /> ); }; describe('Rendering', () => { it('renders the layout with sidebar and content area', () => { renderWithRouter(); // Check for sidebar const sidebar = screen.getByRole('complementary'); expect(sidebar).toBeInTheDocument(); expect(sidebar).toHaveClass('w-64', 'bg-white'); // Check for main content area const main = screen.getByRole('main'); expect(main).toBeInTheDocument(); expect(main).toHaveClass('flex-1', 'overflow-y-auto'); }); it('renders the Settings title', () => { renderWithRouter(); expect(screen.getByText('Settings')).toBeInTheDocument(); }); it('renders all section headings', () => { renderWithRouter(); expect(screen.getByText('Business')).toBeInTheDocument(); expect(screen.getByText('Branding')).toBeInTheDocument(); expect(screen.getByText('Integrations')).toBeInTheDocument(); expect(screen.getByText('Access')).toBeInTheDocument(); expect(screen.getByText('Communication')).toBeInTheDocument(); expect(screen.getByText('Billing')).toBeInTheDocument(); }); it('renders children content from Outlet', () => { renderWithRouter('/settings/general'); expect(screen.getByText('General Settings Content')).toBeInTheDocument(); }); }); describe('Back Button', () => { it('renders the back button', () => { renderWithRouter(); const backButton = screen.getByRole('button', { name: /back to app/i }); expect(backButton).toBeInTheDocument(); }); it('displays ArrowLeft icon in back button', () => { renderWithRouter(); expect(screen.getByTestId('arrow-left-icon')).toBeInTheDocument(); }); it('navigates to home when back button is clicked', () => { renderWithRouter('/settings/general'); const backButton = screen.getByRole('button', { name: /back to app/i }); fireEvent.click(backButton); // Should navigate to home expect(screen.getByText('Home Page')).toBeInTheDocument(); }); it('has correct styling for back button', () => { renderWithRouter(); const backButton = screen.getByRole('button', { name: /back to app/i }); expect(backButton).toHaveClass('text-gray-600', 'hover:text-gray-900', 'transition-colors'); }); }); describe('Navigation Links', () => { describe('Business Section', () => { it('renders General settings link', () => { renderWithRouter(); const generalLink = screen.getByRole('link', { name: /General/i }); expect(generalLink).toBeInTheDocument(); expect(generalLink).toHaveAttribute('href', '/settings/general'); }); it('renders Resource Types settings link', () => { renderWithRouter(); const resourceTypesLink = screen.getByRole('link', { name: /Resource Types/i }); expect(resourceTypesLink).toBeInTheDocument(); expect(resourceTypesLink).toHaveAttribute('href', '/settings/resource-types'); }); it('renders Booking settings link', () => { renderWithRouter(); const bookingLink = screen.getByRole('link', { name: /Booking/i }); expect(bookingLink).toBeInTheDocument(); expect(bookingLink).toHaveAttribute('href', '/settings/booking'); }); it('displays icons for Business section links', () => { renderWithRouter(); expect(screen.getByTestId('building2-icon')).toBeInTheDocument(); expect(screen.getByTestId('layers-icon')).toBeInTheDocument(); expect(screen.getByTestId('calendar-icon')).toBeInTheDocument(); }); }); describe('Branding Section', () => { it('renders Appearance settings link', () => { renderWithRouter(); const appearanceLink = screen.getByRole('link', { name: /Appearance/i }); expect(appearanceLink).toBeInTheDocument(); expect(appearanceLink).toHaveAttribute('href', '/settings/branding'); }); it('renders Email Templates settings link', () => { renderWithRouter(); const emailTemplatesLink = screen.getByRole('link', { name: /Email Templates/i }); expect(emailTemplatesLink).toBeInTheDocument(); expect(emailTemplatesLink).toHaveAttribute('href', '/settings/email-templates'); }); it('renders Custom Domains settings link', () => { renderWithRouter(); const customDomainsLink = screen.getByRole('link', { name: /Custom Domains/i }); expect(customDomainsLink).toBeInTheDocument(); expect(customDomainsLink).toHaveAttribute('href', '/settings/custom-domains'); }); it('displays icons for Branding section links', () => { renderWithRouter(); expect(screen.getByTestId('palette-icon')).toBeInTheDocument(); expect(screen.getAllByTestId('mail-icon').length).toBeGreaterThan(0); expect(screen.getByTestId('globe-icon')).toBeInTheDocument(); }); }); describe('Integrations Section', () => { it('renders API & Webhooks settings link', () => { renderWithRouter(); const apiLink = screen.getByRole('link', { name: /API & Webhooks/i }); expect(apiLink).toBeInTheDocument(); expect(apiLink).toHaveAttribute('href', '/settings/api'); }); it('displays Key icon for API link', () => { renderWithRouter(); expect(screen.getByTestId('key-icon')).toBeInTheDocument(); }); }); describe('Access Section', () => { it('renders Authentication settings link', () => { renderWithRouter(); const authLink = screen.getByRole('link', { name: /Authentication/i }); expect(authLink).toBeInTheDocument(); expect(authLink).toHaveAttribute('href', '/settings/authentication'); }); it('displays Lock icon for Authentication link', () => { renderWithRouter(); expect(screen.getAllByTestId('lock-icon').length).toBeGreaterThan(0); }); }); describe('Communication Section', () => { it('renders Email Setup settings link', () => { renderWithRouter(); const emailSetupLink = screen.getByRole('link', { name: /Email Setup/i }); expect(emailSetupLink).toBeInTheDocument(); expect(emailSetupLink).toHaveAttribute('href', '/settings/email'); }); it('renders SMS & Calling settings link', () => { renderWithRouter(); const smsLink = screen.getByRole('link', { name: /SMS & Calling/i }); expect(smsLink).toBeInTheDocument(); expect(smsLink).toHaveAttribute('href', '/settings/sms-calling'); }); it('displays Phone icon for SMS & Calling link', () => { renderWithRouter(); expect(screen.getByTestId('phone-icon')).toBeInTheDocument(); }); }); describe('Billing Section', () => { it('renders Plan & Billing settings link', () => { renderWithRouter(); const billingLink = screen.getByRole('link', { name: /Plan & Billing/i }); expect(billingLink).toBeInTheDocument(); expect(billingLink).toHaveAttribute('href', '/settings/billing'); }); it('renders Quota Management settings link', () => { renderWithRouter(); const quotaLink = screen.getByRole('link', { name: /Quota Management/i }); expect(quotaLink).toBeInTheDocument(); expect(quotaLink).toHaveAttribute('href', '/settings/quota'); }); it('displays icons for Billing section links', () => { renderWithRouter(); expect(screen.getByTestId('credit-card-icon')).toBeInTheDocument(); expect(screen.getByTestId('alert-triangle-icon')).toBeInTheDocument(); }); }); }); describe('Active Section Highlighting', () => { it('highlights the General link when on /settings/general', () => { renderWithRouter('/settings/general'); const generalLink = screen.getByRole('link', { name: /General/i }); expect(generalLink).toHaveClass('bg-brand-50', 'text-brand-700'); }); it('highlights the Branding link when on /settings/branding', () => { renderWithRouter('/settings/branding'); const brandingLink = screen.getByRole('link', { name: /Appearance/i }); expect(brandingLink).toHaveClass('bg-brand-50', 'text-brand-700'); }); it('highlights the API link when on /settings/api', () => { renderWithRouter('/settings/api'); const apiLink = screen.getByRole('link', { name: /API & Webhooks/i }); expect(apiLink).toHaveClass('bg-brand-50', 'text-brand-700'); }); it('highlights the Billing link when on /settings/billing', () => { renderWithRouter('/settings/billing'); const billingLink = screen.getByRole('link', { name: /Plan & Billing/i }); expect(billingLink).toHaveClass('bg-brand-50', 'text-brand-700'); }); it('does not highlight links when on different pages', () => { renderWithRouter('/settings/general'); const brandingLink = screen.getByRole('link', { name: /Appearance/i }); expect(brandingLink).not.toHaveClass('bg-brand-50', 'text-brand-700'); expect(brandingLink).toHaveClass('text-gray-600'); }); }); describe('Locked Features', () => { beforeEach(() => { // Reset mock for locked feature tests mockCanUse.mockImplementation((feature: string) => { // Lock specific features if (feature === 'white_label') return false; if (feature === 'custom_domain') return false; if (feature === 'api_access') return false; if (feature === 'custom_oauth') return false; if (feature === 'sms_reminders') return false; return true; }); }); it('shows lock icon for Appearance link when white_label is locked', () => { renderWithRouter(); const appearanceLink = screen.getByRole('link', { name: /Appearance/i }); const lockIcons = within(appearanceLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBeGreaterThan(0); }); it('shows lock icon for Custom Domains link when custom_domain is locked', () => { renderWithRouter(); const customDomainsLink = screen.getByRole('link', { name: /Custom Domains/i }); const lockIcons = within(customDomainsLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBeGreaterThan(0); }); it('shows lock icon for API link when api_access is locked', () => { renderWithRouter(); const apiLink = screen.getByRole('link', { name: /API & Webhooks/i }); const lockIcons = within(apiLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBeGreaterThan(0); }); it('shows lock icon for Authentication link when custom_oauth is locked', () => { renderWithRouter(); const authLink = screen.getByRole('link', { name: /Authentication/i }); const lockIcons = within(authLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBeGreaterThan(0); }); it('shows lock icon for SMS & Calling link when sms_reminders is locked', () => { renderWithRouter(); const smsLink = screen.getByRole('link', { name: /SMS & Calling/i }); const lockIcons = within(smsLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBeGreaterThan(0); }); it('applies locked styling to locked links', () => { renderWithRouter(); const appearanceLink = screen.getByRole('link', { name: /Appearance/i }); expect(appearanceLink).toHaveClass('text-gray-400'); }); it('does not show lock icon for unlocked features', () => { // Reset to all unlocked mockCanUse.mockReturnValue(true); renderWithRouter(); const generalLink = screen.getByRole('link', { name: /General/i }); const lockIcons = within(generalLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBe(0); }); }); describe('Outlet Context', () => { it('passes parent context to child routes', () => { const ChildComponent = () => { const context = require('react-router-dom').useOutletContext(); return (
{context.user?.name}
{context.business?.name}
); }; render( }> } /> ); expect(screen.getByTestId('user-name')).toHaveTextContent('John Doe'); expect(screen.getByTestId('business-name')).toHaveTextContent('Test Business'); }); it('passes isFeatureLocked to child routes when feature is locked', () => { mockCanUse.mockImplementation((feature: string) => { return feature !== 'white_label'; }); const ChildComponent = () => { const context = require('react-router-dom').useOutletContext(); return (
{String(context.isFeatureLocked)}
{context.lockedFeature || 'none'}
); }; render( }> } /> ); expect(screen.getByTestId('is-locked')).toHaveTextContent('true'); expect(screen.getByTestId('locked-feature')).toHaveTextContent('white_label'); }); it('passes isFeatureLocked as false when feature is unlocked', () => { mockCanUse.mockReturnValue(true); const ChildComponent = () => { const context = require('react-router-dom').useOutletContext(); return
{String(context.isFeatureLocked)}
; }; render( }> } /> ); expect(screen.getByTestId('is-locked')).toHaveTextContent('false'); }); }); describe('Layout Structure', () => { it('has proper flexbox layout', () => { renderWithRouter(); const layout = screen.getByRole('complementary').parentElement; expect(layout).toHaveClass('flex', 'h-full'); }); it('sidebar has correct width and styling', () => { renderWithRouter(); const sidebar = screen.getByRole('complementary'); expect(sidebar).toHaveClass('w-64', 'bg-white', 'border-r', 'flex', 'flex-col', 'shrink-0'); }); it('main content area has proper overflow handling', () => { renderWithRouter(); const main = screen.getByRole('main'); expect(main).toHaveClass('flex-1', 'overflow-y-auto'); }); it('content is constrained with max-width', () => { renderWithRouter(); const contentWrapper = screen.getByText('General Settings Content').parentElement; expect(contentWrapper).toHaveClass('max-w-4xl', 'mx-auto', 'p-8'); }); }); describe('Link Descriptions', () => { it('displays description for General settings', () => { renderWithRouter(); expect(screen.getByText('Name, timezone, contact')).toBeInTheDocument(); }); it('displays description for Resource Types settings', () => { renderWithRouter(); expect(screen.getByText('Staff, rooms, equipment')).toBeInTheDocument(); }); it('displays description for Appearance settings', () => { renderWithRouter(); expect(screen.getByText('Logo, colors, theme')).toBeInTheDocument(); }); it('displays description for API settings', () => { renderWithRouter(); expect(screen.getByText('API tokens, webhooks')).toBeInTheDocument(); }); it('displays description for Billing settings', () => { renderWithRouter(); expect(screen.getByText('Subscription, invoices')).toBeInTheDocument(); }); }); describe('Dark Mode Support', () => { it('has dark mode classes on sidebar', () => { renderWithRouter(); const sidebar = screen.getByRole('complementary'); expect(sidebar).toHaveClass('dark:bg-gray-800', 'dark:border-gray-700'); }); it('has dark mode classes on Settings title', () => { renderWithRouter(); const title = screen.getByText('Settings'); expect(title).toHaveClass('dark:text-white'); }); it('has dark mode classes on layout background', () => { renderWithRouter(); const layout = screen.getByRole('complementary').parentElement; expect(layout).toHaveClass('dark:bg-gray-900'); }); }); describe('Accessibility', () => { it('uses semantic HTML with aside element', () => { renderWithRouter(); const sidebar = screen.getByRole('complementary'); expect(sidebar.tagName).toBe('ASIDE'); }); it('uses semantic HTML with main element', () => { renderWithRouter(); const main = screen.getByRole('main'); expect(main.tagName).toBe('MAIN'); }); it('uses semantic HTML with nav element', () => { renderWithRouter(); const nav = screen.getByRole('navigation'); expect(nav.tagName).toBe('NAV'); }); it('all navigation links are keyboard accessible', () => { renderWithRouter(); const links = screen.getAllByRole('link'); links.forEach(link => { expect(link).toHaveAttribute('href'); }); }); it('back button is keyboard accessible', () => { renderWithRouter(); const backButton = screen.getByRole('button', { name: /back to app/i }); expect(backButton.tagName).toBe('BUTTON'); }); }); describe('Edge Cases', () => { it('handles navigation between different settings pages', () => { const { rerender } = renderWithRouter('/settings/general'); expect(screen.getByText('General Settings Content')).toBeInTheDocument(); // Navigate to branding render( }> Branding Settings Content} /> ); expect(screen.getByText('Branding Settings Content')).toBeInTheDocument(); }); it('handles all features being locked', () => { mockCanUse.mockReturnValue(false); renderWithRouter(); // Should still render all links, just with locked styling expect(screen.getByRole('link', { name: /Appearance/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /Custom Domains/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /API & Webhooks/i })).toBeInTheDocument(); }); it('handles all features being unlocked', () => { mockCanUse.mockReturnValue(true); renderWithRouter(); // Lock icons should not be visible const appearanceLink = screen.getByRole('link', { name: /Appearance/i }); const lockIcons = within(appearanceLink).queryAllByTestId('lock-icon'); expect(lockIcons.length).toBe(0); }); it('renders without crashing when no route matches', () => { expect(() => renderWithRouter('/settings/nonexistent')).not.toThrow(); }); }); });