Files
ReadMeABook/tests/app/admin.page.test.tsx
T
kikootwo 770cd5165f Mock router and adjust recent requests tests
Update admin page tests to mock next/navigation (useRouter, usePathname, useSearchParams) for the RecentRequestsTable component and reset router mocks in beforeEach. Adjust SWR test fixtures to match the component's real API usage by replacing /api/admin/requests/recent with the paginated endpoint (/api/admin/requests?page=1&pageSize=25&search=&status=all&userId=&sortBy=createdAt&sortOrder=desc), include pagination metadata (total, page, pageSize, totalPages) and request.userId, and add a mock for /api/admin/users. This aligns the tests with the component's routing and data fetching behavior.
2026-02-02 10:42:43 -05:00

193 lines
6.1 KiB
TypeScript

/**
* Component: Admin Dashboard Page Tests
* Documentation: documentation/admin-dashboard.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import AdminDashboard from '@/app/admin/page';
const authenticatedFetcherMock = vi.hoisted(() => vi.fn());
const fetchJSONMock = vi.hoisted(() => vi.fn());
const mutateMock = vi.hoisted(() => vi.fn());
// Mock next/navigation for RecentRequestsTable component
const mockRouter = vi.hoisted(() => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}));
const mockSearchParams = vi.hoisted(() => new URLSearchParams());
vi.mock('next/navigation', () => ({
useRouter: () => mockRouter,
usePathname: () => '/admin',
useSearchParams: () => mockSearchParams,
}));
const toastMock = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
}));
const swrState = new Map<string, { data?: any; error?: any; mutate?: ReturnType<typeof vi.fn> }>();
vi.mock('swr', () => ({
default: (key: string) => {
return swrState.get(key) || { data: undefined, error: undefined, mutate: vi.fn() };
},
mutate: mutateMock,
}));
vi.mock('@/lib/utils/api', () => ({
authenticatedFetcher: authenticatedFetcherMock,
fetchJSON: fetchJSONMock,
fetchWithAuth: vi.fn(),
}));
vi.mock('@/components/ui/Toast', () => ({
ToastProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useToast: () => toastMock,
}));
vi.mock('@/components/requests/InteractiveTorrentSearchModal', () => ({
InteractiveTorrentSearchModal: () => null,
}));
describe('AdminDashboard', () => {
beforeEach(() => {
swrState.clear();
fetchJSONMock.mockReset();
mutateMock.mockReset();
toastMock.success.mockReset();
toastMock.error.mockReset();
mockRouter.push.mockReset();
mockRouter.replace.mockReset();
});
it('renders metrics, downloads, and recent requests', async () => {
swrState.set('/api/admin/metrics', {
data: {
totalRequests: 12,
activeDownloads: 2,
completedLast30Days: 8,
failedLast30Days: 1,
totalUsers: 4,
systemHealth: { status: 'healthy', issues: [] },
},
});
swrState.set('/api/admin/downloads/active', {
data: {
downloads: [
{
requestId: 'r1',
title: 'Active Book',
author: 'Author One',
progress: 55,
speed: 1024,
eta: 1200,
user: 'Zach',
startedAt: new Date('2024-01-01T00:00:00Z'),
},
],
},
});
// RecentRequestsTable fetches from /api/admin/requests with query params
swrState.set('/api/admin/requests?page=1&pageSize=25&search=&status=all&userId=&sortBy=createdAt&sortOrder=desc', {
data: {
requests: [
{
requestId: 'req-1',
title: 'Recent Book',
author: 'Author Two',
status: 'pending',
userId: 'user-1',
user: 'Sam',
createdAt: new Date('2024-01-02T00:00:00Z'),
completedAt: null,
errorMessage: null,
},
],
total: 1,
page: 1,
pageSize: 25,
totalPages: 1,
},
});
swrState.set('/api/admin/users', { data: { users: [] } });
swrState.set('/api/admin/requests/pending-approval', { data: { requests: [] } });
swrState.set('/api/admin/settings', { data: { ebook: { enabled: false } } });
render(<AdminDashboard />);
expect(await screen.findByText('Admin Dashboard')).toBeInTheDocument();
expect(screen.getByText('Total Requests')).toBeInTheDocument();
expect(screen.getByText('Active Book')).toBeInTheDocument();
expect(screen.getByText('Recent Book')).toBeInTheDocument();
});
it('approves a pending request and refreshes caches', async () => {
swrState.set('/api/admin/metrics', {
data: {
totalRequests: 1,
activeDownloads: 0,
completedLast30Days: 0,
failedLast30Days: 0,
totalUsers: 1,
systemHealth: { status: 'healthy', issues: [] },
},
});
swrState.set('/api/admin/downloads/active', { data: { downloads: [] } });
// RecentRequestsTable fetches from /api/admin/requests with query params
swrState.set('/api/admin/requests?page=1&pageSize=25&search=&status=all&userId=&sortBy=createdAt&sortOrder=desc', {
data: { requests: [], total: 0, page: 1, pageSize: 25, totalPages: 0 },
});
swrState.set('/api/admin/users', { data: { users: [] } });
swrState.set('/api/admin/settings', { data: { ebook: { enabled: false } } });
swrState.set('/api/admin/requests/pending-approval', {
data: {
requests: [
{
id: 'pending-1',
createdAt: new Date().toISOString(),
audiobook: { title: 'Awaiting', author: 'Author', coverArtUrl: null },
user: { id: 'u1', plexUsername: 'User', avatarUrl: null },
},
],
},
});
fetchJSONMock.mockResolvedValue({ success: true });
render(<AdminDashboard />);
fireEvent.click(await screen.findByRole('button', { name: 'Approve' }));
await waitFor(() => {
expect(fetchJSONMock).toHaveBeenCalledWith('/api/admin/requests/pending-1/approve', {
method: 'POST',
body: JSON.stringify({ action: 'approve' }),
});
expect(toastMock.success).toHaveBeenCalledWith('Request approved');
});
expect(mutateMock).toHaveBeenCalledWith('/api/admin/requests/pending-approval');
expect(mutateMock).toHaveBeenCalledWith('/api/admin/requests/recent');
expect(mutateMock).toHaveBeenCalledWith('/api/admin/metrics');
});
it('shows an error message when dashboard data fails to load', async () => {
swrState.set('/api/admin/metrics', { error: new Error('Metrics unavailable') });
render(<AdminDashboard />);
expect(await screen.findByText('Error Loading Dashboard')).toBeInTheDocument();
expect(screen.getByText('Metrics unavailable')).toBeInTheDocument();
});
});