diff --git a/apps/web/src/components/ui/ComingSoon.test.tsx b/apps/web/src/components/ui/ComingSoon.test.tsx new file mode 100644 index 000000000..832f20525 --- /dev/null +++ b/apps/web/src/components/ui/ComingSoon.test.tsx @@ -0,0 +1,61 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ComingSoon } from './ComingSoon'; + +// Mock i18n hook +vi.mock('@/hooks/useTranslation', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'comingSoon.description': 'This feature is coming soon!', + 'comingSoon.goBack': 'Go Back', + 'comingSoon.notifyMe': 'Notify Me', + }; + return translations[key] || key; + }, + }), +})); + +describe('ComingSoon', () => { + it('should render the feature name', () => { + render(); + + expect(screen.getByText('Audio Streaming')).toBeInTheDocument(); + }); + + it('should render the description', () => { + render(); + + expect(screen.getByText('This feature is coming soon!')).toBeInTheDocument(); + }); + + it('should render the Notify Me button (disabled)', () => { + render(); + + const notifyButton = screen.getByText('Notify Me'); + expect(notifyButton).toBeInTheDocument(); + expect(notifyButton.closest('button')).toBeDisabled(); + }); + + it('should render Go Back button when onGoBack is provided', () => { + const onGoBack = vi.fn(); + render(); + + const goBackButton = screen.getByText('Go Back'); + expect(goBackButton).toBeInTheDocument(); + fireEvent.click(goBackButton); + expect(onGoBack).toHaveBeenCalledTimes(1); + }); + + it('should not render Go Back button when onGoBack is not provided', () => { + render(); + + expect(screen.queryByText('Go Back')).not.toBeInTheDocument(); + }); + + it('should render the logo illustration', () => { + const { container } = render(); + + expect(container.querySelector('svg')).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/ui/ErrorDisplay.test.tsx b/apps/web/src/components/ui/ErrorDisplay.test.tsx new file mode 100644 index 000000000..1dbd23ad6 --- /dev/null +++ b/apps/web/src/components/ui/ErrorDisplay.test.tsx @@ -0,0 +1,102 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { ErrorDisplay } from './ErrorDisplay'; + +// Mock dependencies that ErrorDisplay uses internally +vi.mock('@/utils/toast', () => ({ + default: { success: vi.fn(), error: vi.fn() }, +})); + +vi.mock('@/utils/reportIssue', () => ({ + formatIssueReport: vi.fn(() => 'mock report'), + copyIssueReportToClipboard: vi.fn(), + openGitHubIssue: vi.fn(), +})); + +describe('ErrorDisplay', () => { + it('should render with a string error', () => { + render(); + + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText('Error')).toBeInTheDocument(); + }); + + it('should render with an Error object', () => { + render(); + + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + + it('should render with custom title', () => { + render(); + + expect(screen.getByText('Custom Title')).toBeInTheDocument(); + }); + + it('should render retry button when onRetry is provided', () => { + const onRetry = vi.fn(); + render(); + + const retryButton = screen.getByText('Retry'); + expect(retryButton).toBeInTheDocument(); + fireEvent.click(retryButton); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it('should not render retry button when onRetry is not provided', () => { + render(); + + expect(screen.queryByText('Retry')).not.toBeInTheDocument(); + }); + + it('should render dismiss button when dismissible', () => { + const onDismiss = vi.fn(); + render(); + + const dismissButton = screen.getByLabelText('Dismiss error'); + expect(dismissButton).toBeInTheDocument(); + fireEvent.click(dismissButton); + expect(onDismiss).toHaveBeenCalledTimes(1); + }); + + it('should render with warning severity', () => { + render(); + + expect(screen.getByText('Warning')).toBeInTheDocument(); + }); + + it('should render with info severity', () => { + render(); + + expect(screen.getByText('Information')).toBeInTheDocument(); + }); + + it('should render custom actions', () => { + const onAction = vi.fn(); + render( + , + ); + + const actionButton = screen.getByText('Custom Action'); + expect(actionButton).toBeInTheDocument(); + fireEvent.click(actionButton); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('should render with context-based title', () => { + render( + , + ); + + expect(screen.getByText('Error loading tracks')).toBeInTheDocument(); + }); + + it('should have aria-live polite attribute', () => { + render(); + + expect(screen.getByRole('alert')).toHaveAttribute('aria-live', 'polite'); + }); +}); diff --git a/apps/web/src/components/ui/LoadingState.test.tsx b/apps/web/src/components/ui/LoadingState.test.tsx new file mode 100644 index 000000000..2def0d287 --- /dev/null +++ b/apps/web/src/components/ui/LoadingState.test.tsx @@ -0,0 +1,120 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { LoadingState, LoadingStateWrapper } from './LoadingState'; + +describe('LoadingState', () => { + describe('spinner variant (default)', () => { + it('should render spinner when isLoading is true', () => { + render(); + + expect(screen.getByRole('status')).toBeInTheDocument(); + }); + + it('should render default text', () => { + render(); + + expect(screen.getByText('Chargement...', { selector: 'p' })).toBeInTheDocument(); + }); + + it('should render custom text', () => { + render(); + + expect(screen.getByText('Loading data...', { selector: 'p' })).toBeInTheDocument(); + }); + + it('should render children when isLoading is false', () => { + render( + +
Content loaded
+
, + ); + + expect(screen.getByText('Content loaded')).toBeInTheDocument(); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + }); + + describe('inline variant', () => { + it('should render inline spinner with text', () => { + render(); + + expect(screen.getByText('Saving...')).toBeInTheDocument(); + }); + }); + + describe('skeleton variant', () => { + it('should render skeleton placeholders', () => { + const { container } = render(); + + expect(container.querySelector('.animate-pulse')).toBeInTheDocument(); + }); + + it('should render children as skeleton content', () => { + render( + +
Skeleton child
+
, + ); + + expect(screen.getByTestId('skeleton-content')).toBeInTheDocument(); + }); + }); + + describe('minimal variant', () => { + it('should render minimal spinner with role status', () => { + render(); + + expect(screen.getByRole('status')).toBeInTheDocument(); + }); + }); + + describe('showSkeleton prop', () => { + it('should show skeleton when showSkeleton is true', () => { + const { container } = render(); + + expect(container.querySelector('.animate-pulse')).toBeInTheDocument(); + }); + }); + + it('should apply custom className', () => { + const { container } = render( + , + ); + + expect(container.querySelector('.custom-class')).toBeInTheDocument(); + }); +}); + +describe('LoadingStateWrapper', () => { + it('should show loading state when isLoading is true', () => { + render( + +
Child content
+
, + ); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect(screen.queryByText('Child content')).not.toBeInTheDocument(); + }); + + it('should show children when isLoading is false', () => { + render( + +
Child content
+
, + ); + + expect(screen.getByText('Child content')).toBeInTheDocument(); + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }); + + it('should support custom loading variant', () => { + render( + +
Child
+
, + ); + + expect(screen.getByText('Wait...')).toBeInTheDocument(); + }); +});