/** * Unit tests for WidgetConfigModal component * * Tests cover: * - Component rendering and visibility * - Modal open/close behavior * - Widget list display * - Widget toggle functionality * - Active widget highlighting * - Reset layout functionality * - Widget icons display * - Internationalization (i18n) * - Accessibility * - Backdrop click handling */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import WidgetConfigModal from '../WidgetConfigModal'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => { const translations: Record = { 'dashboard.configureWidgets': 'Configure Widgets', 'dashboard.configureWidgetsDescription': 'Choose which widgets to display on your dashboard', 'dashboard.resetToDefault': 'Reset to Default', 'dashboard.done': 'Done', // Widget titles 'dashboard.widgetTitles.appointmentsMetric': 'Total Appointments', 'dashboard.widgetTitles.customersMetric': 'Active Customers', 'dashboard.widgetTitles.servicesMetric': 'Services', 'dashboard.widgetTitles.resourcesMetric': 'Resources', 'dashboard.widgetTitles.revenueChart': 'Revenue', 'dashboard.widgetTitles.appointmentsChart': 'Appointments Trend', 'dashboard.widgetTitles.openTickets': 'Open Tickets', 'dashboard.widgetTitles.recentActivity': 'Recent Activity', 'dashboard.widgetTitles.capacityUtilization': 'Capacity Utilization', 'dashboard.widgetTitles.noShowRate': 'No-Show Rate', 'dashboard.widgetTitles.customerBreakdown': 'New vs Returning', // Widget descriptions 'dashboard.widgetDescriptions.appointmentsMetric': 'Shows appointment count with weekly and monthly growth', 'dashboard.widgetDescriptions.customersMetric': 'Shows customer count with weekly and monthly growth', 'dashboard.widgetDescriptions.servicesMetric': 'Shows number of services offered', 'dashboard.widgetDescriptions.resourcesMetric': 'Shows number of resources available', 'dashboard.widgetDescriptions.revenueChart': 'Weekly revenue bar chart', 'dashboard.widgetDescriptions.appointmentsChart': 'Weekly appointments line chart', 'dashboard.widgetDescriptions.openTickets': 'Shows open support tickets requiring attention', 'dashboard.widgetDescriptions.recentActivity': 'Timeline of recent business events', 'dashboard.widgetDescriptions.capacityUtilization': 'Shows how booked your resources are this week', 'dashboard.widgetDescriptions.noShowRate': 'Percentage of appointments marked as no-show', 'dashboard.widgetDescriptions.customerBreakdown': 'Customer breakdown this month', }; return translations[key] || key; }, }), })); describe('WidgetConfigModal', () => { const mockOnClose = vi.fn(); const mockOnToggleWidget = vi.fn(); const mockOnResetLayout = vi.fn(); const defaultProps = { isOpen: true, onClose: mockOnClose, activeWidgets: ['appointments-metric', 'customers-metric', 'revenue-chart'], onToggleWidget: mockOnToggleWidget, onResetLayout: mockOnResetLayout, }; beforeEach(() => { vi.clearAllMocks(); }); describe('Modal Visibility', () => { it('should render when isOpen is true', () => { render(); expect(screen.getByText('Configure Widgets')).toBeInTheDocument(); }); it('should not render when isOpen is false', () => { render(); expect(screen.queryByText('Configure Widgets')).not.toBeInTheDocument(); }); it('should return null when not open', () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); }); describe('Modal Header', () => { it('should render modal title', () => { render(); const title = screen.getByText('Configure Widgets'); expect(title).toBeInTheDocument(); expect(title).toHaveClass('text-lg', 'font-semibold'); }); it('should render close button in header', () => { const { container } = render(); // Close button (X icon) should be present const closeButtons = container.querySelectorAll('button'); expect(closeButtons.length).toBeGreaterThan(0); }); it('should call onClose when header close button is clicked', async () => { const user = userEvent.setup(); const { container } = render(); // Find the X button in header const buttons = container.querySelectorAll('button'); const closeButton = Array.from(buttons).find(btn => btn.querySelector('svg') ) as HTMLElement; if (closeButton) { await user.click(closeButton); expect(mockOnClose).toHaveBeenCalledTimes(1); } }); }); describe('Modal Content', () => { it('should render description text', () => { render(); expect(screen.getByText('Choose which widgets to display on your dashboard')).toBeInTheDocument(); }); it('should render all widget options', () => { render(); // Check for widget titles expect(screen.getByText('Total Appointments')).toBeInTheDocument(); expect(screen.getByText('Active Customers')).toBeInTheDocument(); expect(screen.getByText('Services')).toBeInTheDocument(); expect(screen.getByText('Resources')).toBeInTheDocument(); expect(screen.getByText('Revenue')).toBeInTheDocument(); expect(screen.getByText('Appointments Trend')).toBeInTheDocument(); expect(screen.getByText('Open Tickets')).toBeInTheDocument(); expect(screen.getByText('Recent Activity')).toBeInTheDocument(); expect(screen.getByText('Capacity Utilization')).toBeInTheDocument(); expect(screen.getByText('No-Show Rate')).toBeInTheDocument(); expect(screen.getByText('New vs Returning')).toBeInTheDocument(); }); it('should render widget descriptions', () => { render(); expect(screen.getByText('Shows appointment count with weekly and monthly growth')).toBeInTheDocument(); expect(screen.getByText('Timeline of recent business events')).toBeInTheDocument(); }); it('should render widget icons', () => { const { container } = render(); // Should have multiple SVG icons const svgs = container.querySelectorAll('svg'); expect(svgs.length).toBeGreaterThan(10); // At least one per widget }); }); describe('Widget Selection', () => { it('should highlight active widgets', () => { const { container } = render(); // Active widgets should have brand-500 border const activeWidgets = container.querySelectorAll('.border-brand-500'); expect(activeWidgets.length).toBe(defaultProps.activeWidgets.length); }); it('should show checkmark on active widgets', () => { render(); // Check icons should be present for active widgets const { container } = render(); const svgs = container.querySelectorAll('svg'); // Should have check icons (hard to test exact count due to other icons) expect(svgs.length).toBeGreaterThan(0); }); it('should not highlight inactive widgets', () => { const { container } = render(); // Inactive widgets should have gray border const inactiveWidgets = container.querySelectorAll('.border-gray-200'); expect(inactiveWidgets.length).toBeGreaterThan(0); }); it('should call onToggleWidget when widget is clicked', async () => { const user = userEvent.setup(); render(); const widget = screen.getByText('Total Appointments').closest('button'); expect(widget).toBeInTheDocument(); if (widget) { await user.click(widget); expect(mockOnToggleWidget).toHaveBeenCalledWith('appointments-metric'); } }); it('should call onToggleWidget with correct widget ID', async () => { const user = userEvent.setup(); render(); const revenueWidget = screen.getByText('Revenue').closest('button'); if (revenueWidget) { await user.click(revenueWidget); expect(mockOnToggleWidget).toHaveBeenCalledWith('revenue-chart'); } const ticketsWidget = screen.getByText('Open Tickets').closest('button'); if (ticketsWidget) { await user.click(ticketsWidget); expect(mockOnToggleWidget).toHaveBeenCalledWith('open-tickets'); } }); it('should allow toggling multiple widgets', async () => { const user = userEvent.setup(); render(); const widget1 = screen.getByText('Services').closest('button'); const widget2 = screen.getByText('Resources').closest('button'); if (widget1) await user.click(widget1); if (widget2) await user.click(widget2); expect(mockOnToggleWidget).toHaveBeenCalledTimes(2); }); }); describe('Active Widget Styling', () => { it('should apply active styling to selected widgets', () => { const { container } = render(); // Active widgets should have brand colors const brandBg = container.querySelectorAll('.bg-brand-50'); expect(brandBg.length).toBeGreaterThan(0); }); it('should apply inactive styling to unselected widgets', () => { const { container } = render( ); // Many widgets should have gray styling const grayBorders = container.querySelectorAll('.border-gray-200'); expect(grayBorders.length).toBeGreaterThan(5); }); it('should apply different icon colors for active vs inactive', () => { const { container } = render(); // Active widgets should have brand icon colors const brandIcons = container.querySelectorAll('.text-brand-600'); expect(brandIcons.length).toBeGreaterThan(0); // Inactive widgets should have gray icon colors const grayIcons = container.querySelectorAll('.text-gray-500'); expect(grayIcons.length).toBeGreaterThan(0); }); }); describe('Modal Footer', () => { it('should render reset button', () => { render(); expect(screen.getByText('Reset to Default')).toBeInTheDocument(); }); it('should render done button', () => { render(); expect(screen.getByText('Done')).toBeInTheDocument(); }); it('should call onResetLayout when reset button is clicked', async () => { const user = userEvent.setup(); render(); const resetButton = screen.getByText('Reset to Default'); await user.click(resetButton); expect(mockOnResetLayout).toHaveBeenCalledTimes(1); }); it('should call onClose when done button is clicked', async () => { const user = userEvent.setup(); render(); const doneButton = screen.getByText('Done'); await user.click(doneButton); expect(mockOnClose).toHaveBeenCalledTimes(1); }); }); describe('Backdrop Interaction', () => { it('should render backdrop', () => { const { container } = render(); // Backdrop div with bg-black/50 const backdrop = container.querySelector('.bg-black\\/50'); expect(backdrop).toBeInTheDocument(); }); it('should call onClose when backdrop is clicked', async () => { const user = userEvent.setup(); const { container } = render(); const backdrop = container.querySelector('.bg-black\\/50') as HTMLElement; expect(backdrop).toBeInTheDocument(); if (backdrop) { await user.click(backdrop); expect(mockOnClose).toHaveBeenCalledTimes(1); } }); it('should not call onClose when modal content is clicked', async () => { const user = userEvent.setup(); const { container } = render(); // Click on modal content, not backdrop const modalContent = container.querySelector('.bg-white') as HTMLElement; if (modalContent) { await user.click(modalContent); expect(mockOnClose).not.toHaveBeenCalled(); } }); }); describe('Widget Grid Layout', () => { it('should display widgets in a grid', () => { const { container } = render(); // Grid container should exist const grid = container.querySelector('.grid'); expect(grid).toBeInTheDocument(); expect(grid).toHaveClass('grid-cols-1', 'sm:grid-cols-2'); }); it('should render all 11 widgets', () => { render(); // Count widget buttons const widgetButtons = screen.getAllByRole('button'); // Should have 11 widget buttons + 2 footer buttons + 1 close button = 14 total expect(widgetButtons.length).toBeGreaterThanOrEqual(11); }); }); describe('Styling', () => { it('should apply modal container styles', () => { const { container } = render(); const modal = container.querySelector('.bg-white'); expect(modal).toHaveClass( 'bg-white', 'rounded-xl', 'shadow-xl', 'max-w-2xl', 'w-full' ); }); it('should apply dark mode styles', () => { const { container } = render(); const modal = container.querySelector('.dark\\:bg-gray-800'); expect(modal).toBeInTheDocument(); }); it('should make modal scrollable', () => { const { container } = render(); const scrollableContent = container.querySelector('.overflow-y-auto'); expect(scrollableContent).toBeInTheDocument(); }); it('should apply max height to modal', () => { const { container } = render(); const modal = container.querySelector('.max-h-\\[80vh\\]'); expect(modal).toBeInTheDocument(); }); }); describe('Accessibility', () => { it('should have semantic HTML structure', () => { const { container } = render(); const headings = container.querySelectorAll('h2'); expect(headings.length).toBeGreaterThan(0); }); it('should have accessible buttons', () => { render(); const buttons = screen.getAllByRole('button'); expect(buttons.length).toBeGreaterThan(0); }); it('should have clear button text', () => { render(); expect(screen.getByText('Done')).toBeInTheDocument(); expect(screen.getByText('Reset to Default')).toBeInTheDocument(); }); it('should have descriptive widget names', () => { render(); expect(screen.getByText('Total Appointments')).toBeInTheDocument(); expect(screen.getByText('Recent Activity')).toBeInTheDocument(); }); }); describe('Widget Descriptions', () => { it('should show description for each widget', () => { render(); // Check a few widget descriptions expect(screen.getByText('Shows appointment count with weekly and monthly growth')).toBeInTheDocument(); expect(screen.getByText('Timeline of recent business events')).toBeInTheDocument(); expect(screen.getByText('Shows how booked your resources are this week')).toBeInTheDocument(); }); it('should display descriptions in smaller text', () => { const { container } = render(); const descriptions = container.querySelectorAll('.text-xs'); expect(descriptions.length).toBeGreaterThan(0); }); }); describe('Edge Cases', () => { it('should handle empty activeWidgets array', () => { render(); // Should still render all widgets, just none selected expect(screen.getByText('Total Appointments')).toBeInTheDocument(); // No checkmarks should be visible const { container } = render(); const activeWidgets = container.querySelectorAll('.border-brand-500'); expect(activeWidgets.length).toBe(0); }); it('should handle all widgets active', () => { const allWidgets = [ 'appointments-metric', 'customers-metric', 'services-metric', 'resources-metric', 'revenue-chart', 'appointments-chart', 'open-tickets', 'recent-activity', 'capacity-utilization', 'no-show-rate', 'customer-breakdown', ]; const { container } = render( ); // All widgets should have active styling const activeWidgets = container.querySelectorAll('.border-brand-500'); expect(activeWidgets.length).toBe(11); }); it('should handle rapid widget toggling', async () => { const user = userEvent.setup(); render(); const widget = screen.getByText('Services').closest('button'); if (widget) { await user.click(widget); await user.click(widget); await user.click(widget); expect(mockOnToggleWidget).toHaveBeenCalledTimes(3); } }); }); describe('Internationalization', () => { it('should use translations for modal title', () => { render(); expect(screen.getByText('Configure Widgets')).toBeInTheDocument(); }); it('should use translations for widget titles', () => { render(); expect(screen.getByText('Total Appointments')).toBeInTheDocument(); expect(screen.getByText('Recent Activity')).toBeInTheDocument(); }); it('should use translations for widget descriptions', () => { render(); expect(screen.getByText('Timeline of recent business events')).toBeInTheDocument(); }); it('should use translations for buttons', () => { render(); expect(screen.getByText('Done')).toBeInTheDocument(); expect(screen.getByText('Reset to Default')).toBeInTheDocument(); }); }); describe('Integration', () => { it('should render correctly with all props', () => { const handleClose = vi.fn(); const handleToggle = vi.fn(); const handleReset = vi.fn(); render( ); expect(screen.getByText('Configure Widgets')).toBeInTheDocument(); expect(screen.getByText('Total Appointments')).toBeInTheDocument(); expect(screen.getByText('Done')).toBeInTheDocument(); }); it('should support complete user workflow', async () => { const user = userEvent.setup(); const handleClose = vi.fn(); const handleToggle = vi.fn(); const handleReset = vi.fn(); render( ); // User toggles a widget const widget = screen.getByText('Revenue').closest('button'); if (widget) await user.click(widget); expect(handleToggle).toHaveBeenCalledWith('revenue-chart'); // User resets layout const resetButton = screen.getByText('Reset to Default'); await user.click(resetButton); expect(handleReset).toHaveBeenCalledTimes(1); // User closes modal const doneButton = screen.getByText('Done'); await user.click(doneButton); expect(handleClose).toHaveBeenCalledTimes(1); }); }); });