/** * Unit tests for TrialBanner component * * Tests the trial status banner that appears at the top of the business layout. * Covers: * - Rendering with different days remaining * - Urgent state (3 days or less) * - Upgrade button navigation * - Dismiss functionality * - Hidden states (dismissed, not active, no days left) * - Trial end date formatting */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, within } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import TrialBanner from '../TrialBanner'; import { Business } from '../../types'; // Mock react-router-dom's useNavigate const mockNavigate = vi.fn(); vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); return { ...actual, useNavigate: () => mockNavigate, }; }); // Mock i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, params?: Record) => { // Simulate translation behavior const translations: Record = { 'trial.banner.title': 'Trial Active', 'trial.banner.daysLeft': `${params?.days} days left in trial`, 'trial.banner.expiresOn': `Trial expires on ${params?.date}`, 'trial.banner.upgradeNow': 'Upgrade Now', 'trial.banner.dismiss': 'Dismiss', }; return translations[key] || key; }, }), })); // Test data factory for Business objects const createMockBusiness = (overrides?: Partial): Business => ({ id: '1', name: 'Test Business', subdomain: 'testbiz', primaryColor: '#3B82F6', secondaryColor: '#1E40AF', whitelabelEnabled: false, paymentsEnabled: true, requirePaymentMethodToBook: false, cancellationWindowHours: 24, lateCancellationFeePercent: 50, isTrialActive: true, daysLeftInTrial: 10, trialEnd: '2025-12-17T23:59:59Z', ...overrides, }); // Wrapper component that provides router context const renderWithRouter = (ui: React.ReactElement) => { return render({ui}); }; describe('TrialBanner', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('Rendering', () => { it('should render banner with trial information when trial is active', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, trialEnd: '2025-12-17T23:59:59Z', }); renderWithRouter(); expect(screen.getByText(/trial active/i)).toBeInTheDocument(); expect(screen.getByText(/10 days left in trial/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /upgrade now/i })).toBeInTheDocument(); }); it('should display the trial end date', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 5, trialEnd: '2025-12-17T00:00:00Z', }); renderWithRouter(); // Check that the date is displayed (format may vary by locale) expect(screen.getByText(/trial expires on/i)).toBeInTheDocument(); }); it('should render Sparkles icon when more than 3 days left', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 7, }); const { container } = renderWithRouter(); // The Sparkles icon should be rendered (not the Clock icon) // Check for the non-urgent styling const banner = container.querySelector('.bg-gradient-to-r.from-blue-600'); expect(banner).toBeInTheDocument(); }); it('should render Clock icon with pulse animation when 3 days or less left', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 3, }); const { container } = renderWithRouter(); // Check for urgent styling const banner = container.querySelector('.bg-gradient-to-r.from-red-500'); expect(banner).toBeInTheDocument(); // Check for pulse animation on the icon const pulsingIcon = container.querySelector('.animate-pulse'); expect(pulsingIcon).toBeInTheDocument(); }); it('should render Upgrade Now button with arrow icon', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); const upgradeButton = screen.getByRole('button', { name: /upgrade now/i }); expect(upgradeButton).toBeInTheDocument(); expect(upgradeButton).toHaveClass('bg-white', 'text-blue-600'); }); it('should render dismiss button with aria-label', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); expect(dismissButton).toBeInTheDocument(); expect(dismissButton).toHaveAttribute('aria-label', 'Dismiss'); }); }); describe('Urgent State (3 days or less)', () => { it('should apply urgent styling when 3 days left', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 3, }); const { container } = renderWithRouter(); const banner = container.querySelector('.bg-gradient-to-r.from-red-500.to-orange-500'); expect(banner).toBeInTheDocument(); }); it('should apply urgent styling when 2 days left', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 2, }); const { container } = renderWithRouter(); const banner = container.querySelector('.bg-gradient-to-r.from-red-500.to-orange-500'); expect(banner).toBeInTheDocument(); }); it('should apply urgent styling when 1 day left', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 1, }); const { container } = renderWithRouter(); expect(screen.getByText(/1 days left in trial/i)).toBeInTheDocument(); const banner = container.querySelector('.bg-gradient-to-r.from-red-500.to-orange-500'); expect(banner).toBeInTheDocument(); }); it('should NOT apply urgent styling when 4 days left', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 4, }); const { container } = renderWithRouter(); const banner = container.querySelector('.bg-gradient-to-r.from-blue-600.to-blue-500'); expect(banner).toBeInTheDocument(); expect(container.querySelector('.from-red-500')).not.toBeInTheDocument(); }); }); describe('User Interactions', () => { it('should navigate to /upgrade when Upgrade Now button is clicked', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); const upgradeButton = screen.getByRole('button', { name: /upgrade now/i }); fireEvent.click(upgradeButton); expect(mockNavigate).toHaveBeenCalledWith('/upgrade'); expect(mockNavigate).toHaveBeenCalledTimes(1); }); it('should hide banner when dismiss button is clicked', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); // Banner should be visible initially expect(screen.getByText(/trial active/i)).toBeInTheDocument(); // Click dismiss button const dismissButton = screen.getByRole('button', { name: /dismiss/i }); fireEvent.click(dismissButton); // Banner should be hidden expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); it('should keep banner hidden after dismissing even when multiple clicks', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); fireEvent.click(dismissButton); // Banner should remain hidden expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); }); describe('Hidden States', () => { it('should not render when trial is not active', () => { const business = createMockBusiness({ isTrialActive: false, daysLeftInTrial: 10, }); renderWithRouter(); expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); it('should not render when daysLeftInTrial is undefined', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: undefined, }); renderWithRouter(); expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); it('should not render when daysLeftInTrial is 0', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 0, }); renderWithRouter(); expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); it('should not render when daysLeftInTrial is null', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: null as unknown as number, }); renderWithRouter(); expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); it('should not render when already dismissed', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); // Dismiss the banner const dismissButton = screen.getByRole('button', { name: /dismiss/i }); fireEvent.click(dismissButton); // Banner should not be visible expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); }); }); describe('Edge Cases', () => { it('should handle missing trialEnd date gracefully', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 5, trialEnd: undefined, }); renderWithRouter(); // Banner should still render expect(screen.getByText(/trial active/i)).toBeInTheDocument(); expect(screen.getByText(/5 days left in trial/i)).toBeInTheDocument(); }); it('should handle invalid trialEnd date gracefully', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 5, trialEnd: 'invalid-date', }); renderWithRouter(); // Banner should still render despite invalid date expect(screen.getByText(/trial active/i)).toBeInTheDocument(); }); it('should display correct styling for boundary case of exactly 3 days', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 3, }); const { container } = renderWithRouter(); // Should use urgent styling at exactly 3 days const banner = container.querySelector('.bg-gradient-to-r.from-red-500'); expect(banner).toBeInTheDocument(); }); it('should handle very large number of days remaining', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 999, }); renderWithRouter(); expect(screen.getByText(/999 days left in trial/i)).toBeInTheDocument(); // Should use non-urgent styling const { container } = render(, { wrapper: BrowserRouter }); const banner = container.querySelector('.bg-gradient-to-r.from-blue-600'); expect(banner).toBeInTheDocument(); }); }); describe('Accessibility', () => { it('should have proper button roles and labels', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); renderWithRouter(); const upgradeButton = screen.getByRole('button', { name: /upgrade now/i }); const dismissButton = screen.getByRole('button', { name: /dismiss/i }); expect(upgradeButton).toBeInTheDocument(); expect(dismissButton).toBeInTheDocument(); expect(dismissButton).toHaveAttribute('aria-label'); }); it('should have readable text content for screen readers', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 7, trialEnd: '2025-12-24T23:59:59Z', }); renderWithRouter(); // All important text should be accessible expect(screen.getByText(/trial active/i)).toBeInTheDocument(); expect(screen.getByText(/7 days left in trial/i)).toBeInTheDocument(); expect(screen.getByText(/trial expires on/i)).toBeInTheDocument(); }); }); describe('Responsive Behavior', () => { it('should render trial end date with hidden class for small screens', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, trialEnd: '2025-12-17T23:59:59Z', }); const { container } = renderWithRouter(); // The trial end date paragraph should have 'hidden sm:block' classes const endDateElement = container.querySelector('.hidden.sm\\:block'); expect(endDateElement).toBeInTheDocument(); }); it('should render all key elements in the banner', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); const { container } = renderWithRouter(); // Icon container const iconContainer = container.querySelector('.p-2.rounded-full'); expect(iconContainer).toBeInTheDocument(); // Buttons container const buttonsContainer = screen.getByRole('button', { name: /upgrade now/i }).parentElement; expect(buttonsContainer).toBeInTheDocument(); }); }); describe('Component Integration', () => { it('should work with different business configurations', () => { const businesses = [ createMockBusiness({ daysLeftInTrial: 1, isTrialActive: true }), createMockBusiness({ daysLeftInTrial: 7, isTrialActive: true }), createMockBusiness({ daysLeftInTrial: 14, isTrialActive: true }), ]; businesses.forEach((business) => { const { unmount } = renderWithRouter(); expect(screen.getByText(/trial active/i)).toBeInTheDocument(); unmount(); }); }); it('should maintain state across re-renders when not dismissed', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); const { rerender } = renderWithRouter(); expect(screen.getByText(/trial active/i)).toBeInTheDocument(); // Re-render with updated days const updatedBusiness = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 9, }); rerender( ); expect(screen.getByText(/9 days left in trial/i)).toBeInTheDocument(); }); it('should reset dismissed state on component unmount and remount', () => { const business = createMockBusiness({ isTrialActive: true, daysLeftInTrial: 10, }); const { unmount } = renderWithRouter(); // Dismiss the banner const dismissButton = screen.getByRole('button', { name: /dismiss/i }); fireEvent.click(dismissButton); expect(screen.queryByText(/trial active/i)).not.toBeInTheDocument(); // Unmount and remount unmount(); renderWithRouter(); // Banner should reappear (dismissed state is not persisted) expect(screen.getByText(/trial active/i)).toBeInTheDocument(); }); }); });