Files
flyer-crawler.projectium.com/src/pages/admin/AdminStatsPage.test.tsx

243 lines
7.8 KiB
TypeScript

// src/pages/admin/AdminStatsPage.test.tsx
import React from 'react';
import { render, screen, waitFor, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import { AdminStatsPage } from './AdminStatsPage';
import * as apiClient from '../../services/apiClient';
import type { AppStats } from '../../services/apiClient';
import { createMockAppStats } from '../../tests/utils/mockFactories';
import { StatCard } from '../../components/StatCard';
// The apiClient and logger are now mocked globally via src/tests/setup/tests-setup-unit.ts.
const mockedApiClient = vi.mocked(apiClient);
// Mock the child StatCard component to use the shared mock and allow spying
vi.mock('../../components/StatCard', async () => {
const { MockStatCard } = await import('../../tests/utils/componentMocks');
return { StatCard: vi.fn(MockStatCard) };
});
// Get a reference to the mocked component
const mockedStatCard = StatCard as Mock;
// Helper function to render the component within a router context, as it contains a <Link>
const renderWithRouter = () => {
return render(
<MemoryRouter>
<AdminStatsPage />
</MemoryRouter>,
);
};
describe('AdminStatsPage', () => {
beforeEach(() => {
vi.clearAllMocks();
mockedStatCard.mockClear();
});
it('should render a loading spinner while fetching stats', async () => {
let resolvePromise: (value: Response) => void;
const mockPromise = new Promise<Response>((resolve) => {
resolvePromise = resolve;
});
// Cast to any to bypass strict type checking for the mock return value vs Promise
mockedApiClient.getApplicationStats.mockReturnValue(mockPromise as any);
renderWithRouter();
expect(screen.getByRole('status', { name: /loading/i })).toBeInTheDocument();
await act(async () => {
resolvePromise!(
new Response(
JSON.stringify(
createMockAppStats({
userCount: 0,
flyerCount: 0,
flyerItemCount: 0,
storeCount: 0,
pendingCorrectionCount: 0,
recipeCount: 0,
}),
),
),
);
});
});
it('should display stats cards when data is fetched successfully', async () => {
const mockStats: AppStats = createMockAppStats({
userCount: 123,
flyerCount: 456,
flyerItemCount: 7890,
storeCount: 42,
pendingCorrectionCount: 5,
recipeCount: 150,
});
mockedApiClient.getApplicationStats.mockResolvedValue(new Response(JSON.stringify(mockStats)));
renderWithRouter();
// Wait for the stats to be displayed
await waitFor(() => {
expect(screen.getByText('Total Users')).toBeInTheDocument();
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('Flyers Processed')).toBeInTheDocument();
expect(screen.getByText('456')).toBeInTheDocument();
expect(screen.getByText('Total Flyer Items')).toBeInTheDocument();
expect(screen.getByText('7,890')).toBeInTheDocument(); // Note: toLocaleString() adds a comma
expect(screen.getByText('Stores Tracked')).toBeInTheDocument();
expect(screen.getByText('42')).toBeInTheDocument();
expect(screen.getByText('Pending Corrections')).toBeInTheDocument();
expect(screen.getByText('5')).toBeInTheDocument();
expect(screen.getByText('Total Recipes')).toBeInTheDocument();
expect(screen.getByText('150')).toBeInTheDocument();
});
});
it('should pass the correct props to each StatCard component', async () => {
const mockStats: AppStats = createMockAppStats({
userCount: 123,
flyerCount: 456,
flyerItemCount: 7890,
storeCount: 42,
pendingCorrectionCount: 5,
recipeCount: 150,
});
mockedApiClient.getApplicationStats.mockResolvedValue(new Response(JSON.stringify(mockStats)));
renderWithRouter();
await waitFor(() => {
// Wait for the component to have been called at least once
expect(mockedStatCard).toHaveBeenCalled();
});
// Verify it was called 5 times, once for each stat
expect(mockedStatCard).toHaveBeenCalledTimes(6);
// Check props for each card individually for robustness
expect(mockedStatCard).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Total Users',
value: '123',
}),
undefined,
);
expect(mockedStatCard).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Flyers Processed',
value: '456',
}),
undefined,
);
expect(mockedStatCard).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Total Flyer Items',
value: '7,890',
}),
undefined,
);
expect(mockedStatCard).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Stores Tracked',
value: '42',
}),
undefined,
);
expect(mockedStatCard).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Pending Corrections',
value: '5',
}),
undefined,
);
expect(mockedStatCard).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Total Recipes',
value: '150',
}),
undefined,
);
});
it('should format large numbers with commas for readability', async () => {
const mockStats: AppStats = createMockAppStats({
userCount: 1234567,
flyerCount: 9876,
flyerItemCount: 123456789,
recipeCount: 50000,
});
mockedApiClient.getApplicationStats.mockResolvedValue(new Response(JSON.stringify(mockStats)));
renderWithRouter();
await waitFor(() => {
expect(screen.getByText('1,234,567')).toBeInTheDocument();
expect(screen.getByText('9,876')).toBeInTheDocument();
expect(screen.getByText('123,456,789')).toBeInTheDocument();
expect(screen.getByText('50,000')).toBeInTheDocument();
});
});
it('should correctly display zero values for all stats', async () => {
const mockZeroStats: AppStats = createMockAppStats({
userCount: 0,
flyerCount: 0,
flyerItemCount: 0,
storeCount: 0,
pendingCorrectionCount: 0,
recipeCount: 0,
});
mockedApiClient.getApplicationStats.mockResolvedValue(
new Response(JSON.stringify(mockZeroStats)),
);
renderWithRouter();
await waitFor(() => {
// `getAllByText` will find all instances of '0'. There should be 5.
const zeroValueElements = screen.getAllByText('0');
expect(zeroValueElements).toHaveLength(6);
// Also check that the titles are present to be sure we have the cards.
expect(screen.getByText('Total Users')).toBeInTheDocument();
expect(screen.getByText('Pending Corrections')).toBeInTheDocument();
});
});
it('should display an error message if fetching stats fails', async () => {
const errorMessage = 'Failed to connect to the database.';
mockedApiClient.getApplicationStats.mockRejectedValue(new Error(errorMessage));
renderWithRouter();
// Wait for the error message to appear
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
});
it('should display a generic error message for unknown errors', async () => {
mockedApiClient.getApplicationStats.mockRejectedValue('Unknown error object');
renderWithRouter();
await waitFor(() => {
expect(screen.getByText('An unknown error occurred.')).toBeInTheDocument();
});
});
it('should render a link back to the admin dashboard', async () => {
mockedApiClient.getApplicationStats.mockResolvedValue(
new Response(JSON.stringify(createMockAppStats())),
);
renderWithRouter();
const link = await screen.findByRole('link', { name: /back to admin dashboard/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/admin');
});
});