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.
This commit is contained in:
73
frontend/tests/app/admin/page.test.tsx
Normal file
73
frontend/tests/app/admin/page.test.tsx
Normal file
@@ -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(<AdminPage />);
|
||||
|
||||
expect(screen.getByText('Admin Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders description text', () => {
|
||||
render(<AdminPage />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Manage users, organizations, and system settings')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders users management card', () => {
|
||||
render(<AdminPage />);
|
||||
|
||||
expect(screen.getByText('Users')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Manage user accounts and permissions')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders organizations management card', () => {
|
||||
render(<AdminPage />);
|
||||
|
||||
expect(screen.getByText('Organizations')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('View and manage organizations')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders system settings card', () => {
|
||||
render(<AdminPage />);
|
||||
|
||||
expect(screen.getByText('System')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('System settings and configuration')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays coming soon messages', () => {
|
||||
render(<AdminPage />);
|
||||
|
||||
const comingSoonMessages = screen.getAllByText('Coming soon...');
|
||||
expect(comingSoonMessages).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('renders cards in grid layout', () => {
|
||||
const { container } = render(<AdminPage />);
|
||||
|
||||
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(<AdminPage />);
|
||||
|
||||
const containerDiv = container.querySelector('.container');
|
||||
expect(containerDiv).toBeInTheDocument();
|
||||
expect(containerDiv).toHaveClass('mx-auto', 'px-4', 'py-8');
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
<AuthGuard>
|
||||
<div>Protected Content</div>
|
||||
</AuthGuard>,
|
||||
{ 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', () => {
|
||||
|
||||
103
frontend/tests/components/layout/Skeletons.test.tsx
Normal file
103
frontend/tests/components/layout/Skeletons.test.tsx
Normal file
@@ -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(<HeaderSkeleton />);
|
||||
|
||||
// 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(<HeaderSkeleton />);
|
||||
|
||||
// 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(<HeaderSkeleton />);
|
||||
|
||||
// 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(<AuthLoadingSkeleton />);
|
||||
|
||||
// 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(<AuthLoadingSkeleton />);
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement;
|
||||
expect(wrapper).toHaveClass('flex', 'min-h-screen', 'flex-col');
|
||||
});
|
||||
|
||||
it('renders main content with container', () => {
|
||||
const { container } = render(<AuthLoadingSkeleton />);
|
||||
|
||||
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(<AuthLoadingSkeleton />);
|
||||
|
||||
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(<AuthLoadingSkeleton />);
|
||||
|
||||
// HeaderSkeleton should render a banner role
|
||||
const header = screen.getByRole('banner');
|
||||
expect(header).toBeInTheDocument();
|
||||
|
||||
// Should have skeleton animation
|
||||
const { container } = render(<HeaderSkeleton />);
|
||||
const skeletons = container.querySelectorAll('.animate-pulse');
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('includes Footer component', () => {
|
||||
render(<AuthLoadingSkeleton />);
|
||||
|
||||
// Footer should render with contentinfo role
|
||||
const footer = screen.getByRole('contentinfo');
|
||||
expect(footer).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user