/** * Unit tests for QuotaWarningBanner component * * Tests cover: * - Rendering based on quota overage state * - Critical, urgent, and warning severity levels * - Display of correct percentage and usage information * - Multiple overages display * - Manage Quota button/link functionality * - Dismiss button functionality * - Date formatting * - Internationalization (i18n) * - Accessibility attributes */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BrowserRouter } from 'react-router-dom'; import React from 'react'; import QuotaWarningBanner from '../QuotaWarningBanner'; import { QuotaOverage } from '../../api/auth'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, fallback: string, options?: Record) => { // Handle interpolation for dynamic values if (options) { let result = fallback; Object.entries(options).forEach(([key, value]) => { result = result.replace(`{{${key}}}`, String(value)); }); return result; } return fallback; }, }), })); // Test wrapper with Router const createWrapper = () => { return ({ children }: { children: React.ReactNode }) => ( {children} ); }; // Test data factories const createMockOverage = (overrides?: Partial): QuotaOverage => ({ id: 1, quota_type: 'resources', display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 14, grace_period_ends_at: '2025-12-21T00:00:00Z', ...overrides, }); describe('QuotaWarningBanner', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('Rendering Conditions', () => { it('should not render when overages array is empty', () => { const { container } = render( , { wrapper: createWrapper() } ); expect(container).toBeEmptyDOMElement(); }); it('should not render when overages is null', () => { const { container } = render( , { wrapper: createWrapper() } ); expect(container).toBeEmptyDOMElement(); }); it('should not render when overages is undefined', () => { const { container } = render( , { wrapper: createWrapper() } ); expect(container).toBeEmptyDOMElement(); }); it('should render when quota is near limit (warning state)', () => { const overages = [createMockOverage({ days_remaining: 14 })]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/quota exceeded/i)).toBeInTheDocument(); }); it('should render when quota is critical (1 day remaining)', () => { const overages = [createMockOverage({ days_remaining: 1 })]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/urgent.*automatic archiving tomorrow/i)).toBeInTheDocument(); }); it('should render when quota is urgent (7 days remaining)', () => { const overages = [createMockOverage({ days_remaining: 7 })]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/action required.*7 days left/i)).toBeInTheDocument(); }); }); describe('Severity Levels and Styling', () => { it('should apply warning styles for normal overages (>7 days)', () => { const overages = [createMockOverage({ days_remaining: 14 })]; const { container } = render( , { wrapper: createWrapper() } ); const banner = container.querySelector('div[class*="bg-amber-100"]'); expect(banner).toBeInTheDocument(); }); it('should apply urgent styles for 7 days or less', () => { const overages = [createMockOverage({ days_remaining: 7 })]; const { container } = render( , { wrapper: createWrapper() } ); const banner = container.querySelector('div[class*="bg-amber-500"]'); expect(banner).toBeInTheDocument(); }); it('should apply critical styles for 1 day or less', () => { const overages = [createMockOverage({ days_remaining: 1 })]; const { container } = render( , { wrapper: createWrapper() } ); const banner = container.querySelector('div[class*="bg-red-600"]'); expect(banner).toBeInTheDocument(); }); it('should apply critical styles for 0 days remaining', () => { const overages = [createMockOverage({ days_remaining: 0 })]; const { container } = render( , { wrapper: createWrapper() } ); const banner = container.querySelector('div[class*="bg-red-600"]'); expect(banner).toBeInTheDocument(); }); }); describe('Usage and Percentage Display', () => { it('should display correct overage amount', () => { const overages = [ createMockOverage({ overage_amount: 5, display_name: 'Resources', }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/you have 5 Resources over your plan limit/i)).toBeInTheDocument(); }); it('should display current usage and limit in multi-overage list', () => { const overages = [ createMockOverage({ id: 1, current_usage: 15, allowed_limit: 10, display_name: 'Staff Members', }), createMockOverage({ id: 2, current_usage: 20, allowed_limit: 15, display_name: 'Resources', }), ]; render(, { wrapper: createWrapper(), }); // Usage/limit is shown in the "All overages" list when there are multiple expect(screen.getByText(/Staff Members: 15\/10/)).toBeInTheDocument(); expect(screen.getByText(/Resources: 20\/15/)).toBeInTheDocument(); }); it('should display quota type name', () => { const overages = [ createMockOverage({ display_name: 'Calendar Events', overage_amount: 100, }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/you have 100 Calendar Events over your plan limit/i)).toBeInTheDocument(); }); it('should format and display grace period end date', () => { const overages = [ createMockOverage({ grace_period_ends_at: '2025-12-25T00:00:00Z', }), ]; render(, { wrapper: createWrapper(), }); // Date formatting will depend on locale, but should contain the date components const detailsText = screen.getByText(/grace period ends/i); expect(detailsText).toBeInTheDocument(); }); }); describe('Multiple Overages', () => { it('should display most urgent overage in main message', () => { const overages = [ createMockOverage({ id: 1, days_remaining: 14, display_name: 'Resources' }), createMockOverage({ id: 2, days_remaining: 3, display_name: 'Staff Members' }), createMockOverage({ id: 3, days_remaining: 7, display_name: 'Events' }), ]; render(, { wrapper: createWrapper(), }); // Should show the most urgent (3 days) expect(screen.getByText(/action required.*3 days left/i)).toBeInTheDocument(); }); it('should show additional overages section when multiple overages exist', () => { const overages = [ createMockOverage({ id: 1, days_remaining: 14, display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5 }), createMockOverage({ id: 2, days_remaining: 7, display_name: 'Staff', current_usage: 8, allowed_limit: 5, overage_amount: 3 }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/all overages:/i)).toBeInTheDocument(); }); it('should list all overages with details in the additional section', () => { const overages = [ createMockOverage({ id: 1, display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 14, }), createMockOverage({ id: 2, display_name: 'Staff', current_usage: 8, allowed_limit: 5, overage_amount: 3, days_remaining: 7, }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/Resources: 15\/10/)).toBeInTheDocument(); expect(screen.getByText(/over by 5/)).toBeInTheDocument(); expect(screen.getByText(/Staff: 8\/5/)).toBeInTheDocument(); expect(screen.getByText(/over by 3/)).toBeInTheDocument(); }); it('should not show additional overages section for single overage', () => { const overages = [createMockOverage()]; render(, { wrapper: createWrapper(), }); expect(screen.queryByText(/all overages:/i)).not.toBeInTheDocument(); }); it('should display "expires today" for 0 days remaining in overage list', () => { const overages = [ createMockOverage({ id: 1, days_remaining: 14 }), createMockOverage({ id: 2, days_remaining: 0, display_name: 'Critical Item' }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/expires today!/i)).toBeInTheDocument(); }); }); describe('Manage Quota Button', () => { it('should render Manage Quota link', () => { const overages = [createMockOverage()]; render(, { wrapper: createWrapper(), }); const link = screen.getByRole('link', { name: /manage quota/i }); expect(link).toBeInTheDocument(); }); it('should link to settings/quota page', () => { const overages = [createMockOverage()]; render(, { wrapper: createWrapper(), }); const link = screen.getByRole('link', { name: /manage quota/i }); expect(link).toHaveAttribute('href', '/settings/quota'); }); it('should display external link icon', () => { const overages = [createMockOverage()]; render(, { wrapper: createWrapper(), }); const link = screen.getByRole('link', { name: /manage quota/i }); const icon = link.querySelector('svg'); expect(icon).toBeInTheDocument(); }); it('should apply warning button styles for normal overages', () => { const overages = [createMockOverage({ days_remaining: 14 })]; render(, { wrapper: createWrapper(), }); const link = screen.getByRole('link', { name: /manage quota/i }); expect(link).toHaveClass('bg-amber-600'); }); it('should apply urgent button styles for urgent/critical overages', () => { const overages = [createMockOverage({ days_remaining: 7 })]; render(, { wrapper: createWrapper(), }); const link = screen.getByRole('link', { name: /manage quota/i }); expect(link).toHaveClass('bg-white/20'); }); }); describe('Dismiss Button', () => { it('should render dismiss button when onDismiss prop is provided', () => { const overages = [createMockOverage()]; const onDismiss = vi.fn(); render(, { wrapper: createWrapper(), }); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); expect(dismissButton).toBeInTheDocument(); }); it('should not render dismiss button when onDismiss prop is not provided', () => { const overages = [createMockOverage()]; render(, { wrapper: createWrapper(), }); const dismissButton = screen.queryByRole('button', { name: /dismiss/i }); expect(dismissButton).not.toBeInTheDocument(); }); it('should call onDismiss when dismiss button is clicked', async () => { const user = userEvent.setup(); const overages = [createMockOverage()]; const onDismiss = vi.fn(); render(, { wrapper: createWrapper(), }); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); await user.click(dismissButton); expect(onDismiss).toHaveBeenCalledTimes(1); }); it('should display X icon in dismiss button', () => { const overages = [createMockOverage()]; const onDismiss = vi.fn(); render(, { wrapper: createWrapper(), }); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); const icon = dismissButton.querySelector('svg'); expect(icon).toBeInTheDocument(); }); }); describe('Accessibility', () => { it('should have alert icon with appropriate styling', () => { const overages = [createMockOverage()]; const { container } = render( , { wrapper: createWrapper() } ); // AlertTriangle icon should be present const icon = container.querySelector('svg'); expect(icon).toBeInTheDocument(); }); it('should have accessible label for dismiss button', () => { const overages = [createMockOverage()]; const onDismiss = vi.fn(); render(, { wrapper: createWrapper(), }); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); expect(dismissButton).toHaveAttribute('aria-label', 'Dismiss'); }); it('should use semantic HTML structure', () => { const overages = [createMockOverage({ days_remaining: 14 })]; const { container } = render( , { wrapper: createWrapper() } ); // Should have proper div structure expect(container.querySelector('div')).toBeInTheDocument(); }); it('should have accessible link for Manage Quota', () => { const overages = [createMockOverage()]; render(, { wrapper: createWrapper(), }); const link = screen.getByRole('link', { name: /manage quota/i }); expect(link).toBeInTheDocument(); expect(link.tagName).toBe('A'); }); }); describe('Message Priority', () => { it('should show critical message for 1 day remaining', () => { const overages = [createMockOverage({ days_remaining: 1 })]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/urgent.*automatic archiving tomorrow/i)).toBeInTheDocument(); }); it('should show urgent message for 2-7 days remaining', () => { const overages = [createMockOverage({ days_remaining: 5 })]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/action required.*5 days left/i)).toBeInTheDocument(); }); it('should show warning message for more than 7 days remaining', () => { const overages = [createMockOverage({ days_remaining: 10 })]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/quota exceeded for 1 item/i)).toBeInTheDocument(); }); it('should show count of overages in warning message', () => { const overages = [ createMockOverage({ id: 1, days_remaining: 14 }), createMockOverage({ id: 2, days_remaining: 10 }), createMockOverage({ id: 3, days_remaining: 12 }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/quota exceeded for 3 item/i)).toBeInTheDocument(); }); }); describe('Integration', () => { it('should render complete banner with all elements', () => { const overages = [ createMockOverage({ id: 1, quota_type: 'resources', display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 7, grace_period_ends_at: '2025-12-21T00:00:00Z', }), ]; const onDismiss = vi.fn(); render(, { wrapper: createWrapper(), }); // Check main message expect(screen.getByText(/action required.*7 days left/i)).toBeInTheDocument(); // Check details expect(screen.getByText(/you have 5 Resources over your plan limit/i)).toBeInTheDocument(); // Check Manage Quota link const link = screen.getByRole('link', { name: /manage quota/i }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute('href', '/settings/quota'); // Check dismiss button const dismissButton = screen.getByRole('button', { name: /dismiss/i }); expect(dismissButton).toBeInTheDocument(); // Check icons are present (via SVG elements) const { container } = render(, { wrapper: createWrapper(), }); const icons = container.querySelectorAll('svg'); expect(icons.length).toBeGreaterThan(0); }); it('should handle complex multi-overage scenario', async () => { const user = userEvent.setup(); const overages = [ createMockOverage({ id: 1, display_name: 'Resources', current_usage: 15, allowed_limit: 10, overage_amount: 5, days_remaining: 14, }), createMockOverage({ id: 2, display_name: 'Staff Members', current_usage: 12, allowed_limit: 8, overage_amount: 4, days_remaining: 2, }), createMockOverage({ id: 3, display_name: 'Calendar Events', current_usage: 500, allowed_limit: 400, overage_amount: 100, days_remaining: 7, }), ]; const onDismiss = vi.fn(); render(, { wrapper: createWrapper(), }); // Should show most urgent (2 days) expect(screen.getByText(/action required.*2 days left/i)).toBeInTheDocument(); // Should show all overages section expect(screen.getByText(/all overages:/i)).toBeInTheDocument(); expect(screen.getByText(/Resources: 15\/10/)).toBeInTheDocument(); expect(screen.getByText(/Staff Members: 12\/8/)).toBeInTheDocument(); expect(screen.getByText(/Calendar Events: 500\/400/)).toBeInTheDocument(); // Should be able to dismiss const dismissButton = screen.getByRole('button', { name: /dismiss/i }); await user.click(dismissButton); expect(onDismiss).toHaveBeenCalledTimes(1); }); }); describe('Edge Cases', () => { it('should handle negative days remaining', () => { const overages = [createMockOverage({ days_remaining: -1 })]; render(, { wrapper: createWrapper(), }); // Should treat as critical (0 or less) const { container } = render( , { wrapper: createWrapper() } ); const banner = container.querySelector('div[class*="bg-red-600"]'); expect(banner).toBeInTheDocument(); }); it('should handle very large overage amounts', () => { const overages = [ createMockOverage({ overage_amount: 999999, display_name: 'Events', }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/you have 999999 Events over your plan limit/i)).toBeInTheDocument(); }); it('should handle zero overage amount', () => { const overages = [ createMockOverage({ overage_amount: 0, current_usage: 10, allowed_limit: 10, }), ]; render(, { wrapper: createWrapper(), }); expect(screen.getByText(/you have 0 Resources over your plan limit/i)).toBeInTheDocument(); }); }); });