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();
+ });
+});