forked from cardosofelipe/fast-next-template
Add admin UX improvements, constants refactor, and comprehensive tests
- Introduced constants for admin hooks: `STATS_FETCH_LIMIT`, `DEFAULT_PAGE_LIMIT`, and `STATS_REFETCH_INTERVAL` to enhance readability and maintainability. - Updated query guards to ensure data fetching is restricted to superusers. - Enhanced accessibility across admin components by adding `aria-hidden` attributes and improving focus-visible styles. - Simplified `useAdminStats`, `useAdminUsers`, and `useAdminOrganizations` with shared constants. - Added 403 Forbidden page with proper structure, styling, and tests. - Implemented new tests for admin hooks, DashboardStats, AdminLayout, and ForbiddenPage for better coverage.
This commit is contained in:
169
frontend/tests/app/admin/layout.test.tsx
Normal file
169
frontend/tests/app/admin/layout.test.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Tests for Admin Layout
|
||||
* Verifies layout rendering, auth guard, and accessibility features
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import AdminLayout from '@/app/admin/layout';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/auth/AuthContext');
|
||||
jest.mock('@/components/layout/Header', () => ({
|
||||
Header: () => <header data-testid="header">Header</header>,
|
||||
}));
|
||||
jest.mock('@/components/layout/Footer', () => ({
|
||||
Footer: () => <footer data-testid="footer">Footer</footer>,
|
||||
}));
|
||||
jest.mock('@/components/admin/AdminSidebar', () => ({
|
||||
AdminSidebar: () => <aside data-testid="sidebar">Sidebar</aside>,
|
||||
}));
|
||||
jest.mock('@/components/admin/Breadcrumbs', () => ({
|
||||
Breadcrumbs: () => <div data-testid="breadcrumbs">Breadcrumbs</div>,
|
||||
}));
|
||||
|
||||
// Mock next/navigation
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
}),
|
||||
usePathname: () => '/admin',
|
||||
}));
|
||||
|
||||
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
||||
|
||||
describe('AdminLayout', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
it('renders layout with all components for superuser', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminLayout>
|
||||
<div>Test Content</div>
|
||||
</AdminLayout>,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('footer')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('breadcrumbs')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders skip link with correct attributes', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminLayout>
|
||||
<div>Test Content</div>
|
||||
</AdminLayout>,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const skipLink = screen.getByText('Skip to main content');
|
||||
expect(skipLink).toBeInTheDocument();
|
||||
expect(skipLink).toHaveAttribute('href', '#main-content');
|
||||
expect(skipLink).toHaveClass('sr-only');
|
||||
});
|
||||
|
||||
it('renders main element with id', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<AdminLayout>
|
||||
<div>Test Content</div>
|
||||
</AdminLayout>,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const mainElement = container.querySelector('#main-content');
|
||||
expect(mainElement).toBeInTheDocument();
|
||||
expect(mainElement?.tagName).toBe('MAIN');
|
||||
});
|
||||
|
||||
it('renders children inside main content area', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminLayout>
|
||||
<div data-testid="child-content">Child Content</div>
|
||||
</AdminLayout>,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
const mainElement = screen.getByTestId('child-content').closest('main');
|
||||
expect(mainElement).toHaveAttribute('id', 'main-content');
|
||||
});
|
||||
|
||||
it('applies correct layout structure classes', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { is_superuser: true } as any,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<AdminLayout>
|
||||
<div>Test Content</div>
|
||||
</AdminLayout>,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
// Check root container has min-height class
|
||||
const rootDiv = container.querySelector('.min-h-screen');
|
||||
expect(rootDiv).toBeInTheDocument();
|
||||
expect(rootDiv).toHaveClass('flex', 'flex-col');
|
||||
|
||||
// Check main content area has flex and overflow classes
|
||||
const mainElement = container.querySelector('#main-content');
|
||||
expect(mainElement).toHaveClass('flex-1', 'overflow-y-auto');
|
||||
});
|
||||
});
|
||||
@@ -4,35 +4,44 @@
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import AdminPage from '@/app/admin/page';
|
||||
import { useAdminStats } from '@/lib/api/hooks/useAdmin';
|
||||
|
||||
// Helper function to render with QueryClientProvider
|
||||
function renderWithQueryClient(component: React.ReactElement) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
// Mock the useAdminStats hook
|
||||
jest.mock('@/lib/api/hooks/useAdmin');
|
||||
|
||||
const mockUseAdminStats = useAdminStats as jest.MockedFunction<typeof useAdminStats>;
|
||||
|
||||
// Helper function to render with default mocked stats
|
||||
function renderWithMockedStats() {
|
||||
mockUseAdminStats.mockReturnValue({
|
||||
data: {
|
||||
totalUsers: 100,
|
||||
activeUsers: 80,
|
||||
totalOrganizations: 20,
|
||||
totalSessions: 30,
|
||||
},
|
||||
});
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{component}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
return render(<AdminPage />);
|
||||
}
|
||||
|
||||
describe('AdminPage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders admin dashboard title', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
expect(screen.getByText('Admin Dashboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders description text', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
expect(
|
||||
screen.getByText('Manage users, organizations, and system settings')
|
||||
@@ -40,13 +49,13 @@ describe('AdminPage', () => {
|
||||
});
|
||||
|
||||
it('renders quick actions section', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
expect(screen.getByText('Quick Actions')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders user management card', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
expect(screen.getByText('User Management')).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -55,7 +64,7 @@ describe('AdminPage', () => {
|
||||
});
|
||||
|
||||
it('renders organizations card', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
// Check for the quick actions card (not the stat card)
|
||||
expect(
|
||||
@@ -64,7 +73,7 @@ describe('AdminPage', () => {
|
||||
});
|
||||
|
||||
it('renders system settings card', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
expect(screen.getByText('System Settings')).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -73,7 +82,7 @@ describe('AdminPage', () => {
|
||||
});
|
||||
|
||||
it('renders quick actions in grid layout', () => {
|
||||
renderWithQueryClient(<AdminPage />);
|
||||
renderWithMockedStats();
|
||||
|
||||
// Check for Quick Actions heading which is above the grid
|
||||
expect(screen.getByText('Quick Actions')).toBeInTheDocument();
|
||||
@@ -84,7 +93,7 @@ describe('AdminPage', () => {
|
||||
});
|
||||
|
||||
it('renders with proper container structure', () => {
|
||||
const { container } = renderWithQueryClient(<AdminPage />);
|
||||
const { container } = renderWithMockedStats();
|
||||
|
||||
const containerDiv = container.querySelector('.container');
|
||||
expect(containerDiv).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user