From 0e554ef35e71af4a5eb22b4920e230161d85c003 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sun, 2 Nov 2025 17:07:15 +0100 Subject: [PATCH] Add tests for `AuthGuard`, `Skeleton` components, and `AdminPage` - Enhance `AuthGuard` tests with 150ms delay skeleton rendering. - Add new test files: `Skeletons.test.tsx` to validate skeleton components and `admin/page.test.tsx` for admin dashboard. - Refactor `AuthGuard` tests to utilize `jest.useFakeTimers` for delay simulation. - Improve coverage for loading states, fallback behavior, and rendering logic. --- frontend/tests/app/admin/page.test.tsx | 73 +++++++++++++ .../tests/components/auth/AuthGuard.test.tsx | 68 ++++++++++-- .../components/layout/Skeletons.test.tsx | 103 ++++++++++++++++++ 3 files changed, 234 insertions(+), 10 deletions(-) create mode 100644 frontend/tests/app/admin/page.test.tsx create mode 100644 frontend/tests/components/layout/Skeletons.test.tsx diff --git a/frontend/tests/app/admin/page.test.tsx b/frontend/tests/app/admin/page.test.tsx new file mode 100644 index 0000000..28ea57a --- /dev/null +++ b/frontend/tests/app/admin/page.test.tsx @@ -0,0 +1,73 @@ +/** + * Tests for Admin Dashboard Page + * Verifies rendering of admin page placeholder content + */ + +import { render, screen } from '@testing-library/react'; +import AdminPage from '@/app/admin/page'; + +describe('AdminPage', () => { + it('renders admin dashboard title', () => { + render(); + + expect(screen.getByText('Admin Dashboard')).toBeInTheDocument(); + }); + + it('renders description text', () => { + render(); + + expect( + screen.getByText('Manage users, organizations, and system settings') + ).toBeInTheDocument(); + }); + + it('renders users management card', () => { + render(); + + expect(screen.getByText('Users')).toBeInTheDocument(); + expect( + screen.getByText('Manage user accounts and permissions') + ).toBeInTheDocument(); + }); + + it('renders organizations management card', () => { + render(); + + expect(screen.getByText('Organizations')).toBeInTheDocument(); + expect( + screen.getByText('View and manage organizations') + ).toBeInTheDocument(); + }); + + it('renders system settings card', () => { + render(); + + expect(screen.getByText('System')).toBeInTheDocument(); + expect( + screen.getByText('System settings and configuration') + ).toBeInTheDocument(); + }); + + it('displays coming soon messages', () => { + render(); + + const comingSoonMessages = screen.getAllByText('Coming soon...'); + expect(comingSoonMessages).toHaveLength(3); + }); + + it('renders cards in grid layout', () => { + const { container } = render(); + + const grid = container.querySelector('.grid'); + expect(grid).toBeInTheDocument(); + expect(grid).toHaveClass('gap-4', 'md:grid-cols-2', 'lg:grid-cols-3'); + }); + + it('renders with proper container structure', () => { + const { container } = render(); + + const containerDiv = container.querySelector('.container'); + expect(containerDiv).toBeInTheDocument(); + expect(containerDiv).toHaveClass('mx-auto', 'px-4', 'py-8'); + }); +}); diff --git a/frontend/tests/components/auth/AuthGuard.test.tsx b/frontend/tests/components/auth/AuthGuard.test.tsx index 6031b91..140191e 100644 --- a/frontend/tests/components/auth/AuthGuard.test.tsx +++ b/frontend/tests/components/auth/AuthGuard.test.tsx @@ -3,7 +3,7 @@ * Security-critical: Route protection and access control */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, act } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthGuard } from '@/components/auth/AuthGuard'; @@ -64,6 +64,7 @@ const createWrapper = () => { describe('AuthGuard', () => { beforeEach(() => { jest.clearAllMocks(); + jest.useFakeTimers(); // Reset to default unauthenticated state mockAuthState = { isAuthenticated: false, @@ -76,8 +77,32 @@ describe('AuthGuard', () => { }; }); + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + describe('Loading States', () => { - it('shows loading spinner when auth is loading', () => { + it('shows nothing initially when auth is loading (before 150ms)', () => { + mockAuthState = { + isAuthenticated: false, + isLoading: true, + user: null, + }; + + const { container } = render( + +
Protected Content
+
, + { wrapper: createWrapper() } + ); + + // Before 150ms delay, component returns null (empty) + expect(container.firstChild).toBeNull(); + expect(screen.queryByText('Protected Content')).not.toBeInTheDocument(); + }); + + it('shows skeleton after 150ms when auth is loading', () => { mockAuthState = { isAuthenticated: false, isLoading: true, @@ -91,11 +116,17 @@ describe('AuthGuard', () => { { wrapper: createWrapper() } ); - expect(screen.getByText(/loading/i)).toBeInTheDocument(); + // Fast-forward past the 150ms delay + act(() => { + jest.advanceTimersByTime(150); + }); + + // Skeleton should be visible (check for skeleton structure) + expect(screen.getByRole('banner')).toBeInTheDocument(); // Header skeleton expect(screen.queryByText('Protected Content')).not.toBeInTheDocument(); }); - it('shows loading spinner when user data is loading', () => { + it('shows skeleton after 150ms when user data is loading', () => { mockAuthState = { isAuthenticated: true, isLoading: false, @@ -113,10 +144,16 @@ describe('AuthGuard', () => { { wrapper: createWrapper() } ); - expect(screen.getByText(/loading/i)).toBeInTheDocument(); + // Fast-forward past the 150ms delay + act(() => { + jest.advanceTimersByTime(150); + }); + + // Skeleton should be visible + expect(screen.getByRole('banner')).toBeInTheDocument(); // Header skeleton }); - it('shows custom fallback when provided', () => { + it('shows custom fallback after 150ms when provided', () => { mockAuthState = { isAuthenticated: false, isLoading: true, @@ -130,9 +167,14 @@ describe('AuthGuard', () => { { wrapper: createWrapper() } ); + // Fast-forward past the 150ms delay + act(() => { + jest.advanceTimersByTime(150); + }); + expect(screen.getByText('Please wait...')).toBeInTheDocument(); - // Default spinner should not be shown - expect(screen.queryByRole('status')).not.toBeInTheDocument(); + // Default skeleton should not be shown + expect(screen.queryByRole('banner')).not.toBeInTheDocument(); }); }); @@ -296,7 +338,7 @@ describe('AuthGuard', () => { }); describe('Integration with useMe', () => { - it('shows loading while useMe fetches user data', () => { + it('shows skeleton after 150ms while useMe fetches user data', () => { mockAuthState = { isAuthenticated: true, isLoading: false, @@ -314,7 +356,13 @@ describe('AuthGuard', () => { { wrapper: createWrapper() } ); - expect(screen.getByText(/loading/i)).toBeInTheDocument(); + // Fast-forward past the 150ms delay + act(() => { + jest.advanceTimersByTime(150); + }); + + // Skeleton should be visible + expect(screen.getByRole('banner')).toBeInTheDocument(); // Header skeleton }); it('renders children after useMe completes', () => { diff --git a/frontend/tests/components/layout/Skeletons.test.tsx b/frontend/tests/components/layout/Skeletons.test.tsx new file mode 100644 index 0000000..c9fb966 --- /dev/null +++ b/frontend/tests/components/layout/Skeletons.test.tsx @@ -0,0 +1,103 @@ +/** + * Tests for Skeleton Loading Components + * Verifies structure and rendering of loading placeholders + */ + +import { render, screen } from '@testing-library/react'; +import { HeaderSkeleton } from '@/components/layout/HeaderSkeleton'; +import { AuthLoadingSkeleton } from '@/components/layout/AuthLoadingSkeleton'; + +describe('HeaderSkeleton', () => { + it('renders header skeleton structure', () => { + render(); + + // Check for header element + const header = screen.getByRole('banner'); + expect(header).toBeInTheDocument(); + expect(header).toHaveClass('sticky', 'top-0', 'z-50', 'w-full', 'border-b'); + }); + + it('renders with correct layout structure', () => { + const { container } = render(); + + // Check for container + const contentDiv = container.querySelector('.container'); + expect(contentDiv).toBeInTheDocument(); + + // Check for animated skeleton elements + const skeletonElements = container.querySelectorAll('.animate-pulse'); + expect(skeletonElements.length).toBeGreaterThan(0); + }); + + it('has proper styling classes', () => { + const { container } = render(); + + // Verify backdrop blur and background + const header = screen.getByRole('banner'); + expect(header).toHaveClass('bg-background/95', 'backdrop-blur'); + }); +}); + +describe('AuthLoadingSkeleton', () => { + it('renders full page skeleton structure', () => { + render(); + + // Check for header (via HeaderSkeleton) + expect(screen.getByRole('banner')).toBeInTheDocument(); + + // Check for main content area + expect(screen.getByRole('main')).toBeInTheDocument(); + + // Check for footer (via Footer component) + expect(screen.getByRole('contentinfo')).toBeInTheDocument(); + }); + + it('renders with flex layout', () => { + const { container } = render(); + + const wrapper = container.firstChild as HTMLElement; + expect(wrapper).toHaveClass('flex', 'min-h-screen', 'flex-col'); + }); + + it('renders main content with container', () => { + const { container } = render(); + + const main = screen.getByRole('main'); + expect(main).toHaveClass('flex-1'); + + // Check for container inside main + const contentContainer = main.querySelector('.container'); + expect(contentContainer).toBeInTheDocument(); + }); + + it('renders skeleton placeholders in main content', () => { + const { container } = render(); + + const main = screen.getByRole('main'); + + // Check for animated skeleton elements + const skeletonElements = main.querySelectorAll('.animate-pulse'); + expect(skeletonElements.length).toBeGreaterThan(0); + }); + + it('includes HeaderSkeleton component', () => { + render(); + + // HeaderSkeleton should render a banner role + const header = screen.getByRole('banner'); + expect(header).toBeInTheDocument(); + + // Should have skeleton animation + const { container } = render(); + const skeletons = container.querySelectorAll('.animate-pulse'); + expect(skeletons.length).toBeGreaterThan(0); + }); + + it('includes Footer component', () => { + render(); + + // Footer should render with contentinfo role + const footer = screen.getByRole('contentinfo'); + expect(footer).toBeInTheDocument(); + }); +});