Files
gridpilot.gg/apps/website/app/races/[id]/page.test.tsx
2026-01-06 19:36:03 +01:00

239 lines
7.2 KiB
TypeScript

import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RaceDetailInteractive } from './RaceDetailInteractive';
import type { RaceDetailsViewModel } from '@/lib/view-models/RaceDetailsViewModel';
// Mocks for Next.js navigation
const mockPush = vi.fn();
const mockBack = vi.fn();
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
back: mockBack,
}),
useParams: () => ({ id: 'race-123' }),
}));
// Mock effective driver id hook
vi.mock('@/hooks/useEffectiveDriverId', () => ({
useEffectiveDriverId: () => 'driver-1',
}));
// Mock sponsor mode hook to avoid rendering heavy sponsor card
vi.mock('@/components/sponsors/SponsorInsightsCard', () => ({
__esModule: true,
default: () => <div data-testid="sponsor-insights-mock" />,
MetricBuilders: {
views: vi.fn(() => ({ label: 'Views', value: '100' })),
engagement: vi.fn(() => ({ label: 'Engagement', value: '50%' })),
reach: vi.fn(() => ({ label: 'Reach', value: '1000' })),
},
SlotTemplates: {
race: vi.fn(() => []),
},
useSponsorMode: () => false,
}));
// Mock the new DI hooks
const mockGetRaceDetails = vi.fn();
const mockReopenRace = vi.fn();
const mockFetchLeagueMemberships = vi.fn();
const mockGetMembership = vi.fn();
// Mock race detail hook
vi.mock('@/hooks/race/useRaceDetail', () => ({
useRaceDetail: (raceId: string, driverId: string) => ({
data: mockGetRaceDetails.mock.results[0]?.value || null,
isLoading: false,
isError: false,
isSuccess: !!mockGetRaceDetails.mock.results[0]?.value,
refetch: vi.fn(),
retry: vi.fn(),
}),
}));
// Mock reopen race hook
vi.mock('@/hooks/race/useReopenRace', () => ({
useReopenRace: () => ({
mutateAsync: mockReopenRace,
mutate: mockReopenRace,
isPending: false,
isLoading: false,
}),
}));
// Mock league membership service static method
vi.mock('@/lib/services/leagues/LeagueMembershipService', () => ({
LeagueMembershipService: {
getMembership: mockGetMembership,
fetchLeagueMemberships: mockFetchLeagueMemberships,
setLeagueMemberships: vi.fn(),
clearLeagueMemberships: vi.fn(),
getCachedMembershipsIterator: vi.fn(() => [][Symbol.iterator]()),
getAllMembershipsForDriver: vi.fn(() => []),
getLeagueMembers: vi.fn(() => []),
},
}));
// Mock league membership hook (if used by component)
vi.mock('@/hooks/league/useLeagueMemberships', () => ({
useLeagueMemberships: (leagueId: string, currentUserId: string) => ({
data: mockFetchLeagueMemberships.mock.results[0]?.value || null,
isLoading: false,
isError: false,
isSuccess: !!mockFetchLeagueMemberships.mock.results[0]?.value,
refetch: vi.fn(),
}),
}));
// Mock the useLeagueMembership hook that the component imports
vi.mock('@/hooks/useLeagueMembershipService', () => ({
useLeagueMembership: (leagueId: string, driverId: string) => ({
data: mockGetMembership.mock.results[0]?.value || null,
isLoading: false,
isError: false,
isSuccess: !!mockGetMembership.mock.results[0]?.value,
refetch: vi.fn(),
}),
}));
// We'll use the actual hooks but they will use the mocked services
// The hooks are already mocked above via the service mocks
// Mock league membership utility to control admin vs non-admin behavior
const mockIsOwnerOrAdmin = vi.fn();
vi.mock('@/lib/utilities/LeagueMembershipUtility', () => ({
LeagueMembershipUtility: {
isOwnerOrAdmin: (...args: unknown[]) => mockIsOwnerOrAdmin(...args),
},
}));
const renderWithQueryClient = (ui: React.ReactElement) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
};
const createViewModel = (status: string): RaceDetailsViewModel => {
const canReopenRace = status === 'completed' || status === 'cancelled';
return {
race: {
id: 'race-123',
track: 'Test Track',
car: 'Test Car',
scheduledAt: '2023-12-31T20:00:00Z',
status,
sessionType: 'race',
},
league: {
id: 'league-1',
name: 'Test League',
description: 'Test league description',
settings: {
maxDrivers: 32,
qualifyingFormat: 'open',
},
},
entryList: [],
registration: {
isUserRegistered: false,
canRegister: false,
},
userResult: null,
canReopenRace,
};
};
describe('RaceDetailPage - Re-open Race behavior', () => {
beforeEach(() => {
// Reset all mocks
mockGetRaceDetails.mockReset();
mockReopenRace.mockReset();
mockFetchLeagueMemberships.mockReset();
mockGetMembership.mockReset();
mockIsOwnerOrAdmin.mockReset();
// Set up default mock implementations
mockFetchLeagueMemberships.mockResolvedValue(undefined);
mockGetMembership.mockReturnValue({ role: 'owner' }); // Return owner role by default
});
it('shows Re-open Race button for admin when race is completed and calls reopen + reload on confirm', async () => {
mockIsOwnerOrAdmin.mockReturnValue(true);
const viewModel = createViewModel('completed');
// Mock the hooks to return the right data
mockGetRaceDetails.mockReturnValue(viewModel);
mockGetMembership.mockReturnValue({ role: 'owner' });
mockReopenRace.mockResolvedValue(undefined);
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
renderWithQueryClient(<RaceDetailInteractive />);
// Wait for the component to load and render
await waitFor(() => {
const tracks = screen.getAllByText('Test Track');
expect(tracks.length).toBeGreaterThan(0);
});
// Check if the reopen button is present
const reopenButton = screen.getByText('Re-open Race');
expect(reopenButton).toBeInTheDocument();
fireEvent.click(reopenButton);
await waitFor(() => {
expect(mockReopenRace).toHaveBeenCalledWith('race-123');
});
confirmSpy.mockRestore();
});
it('does not render Re-open Race button for non-admin viewer', async () => {
mockIsOwnerOrAdmin.mockReturnValue(false);
const viewModel = createViewModel('completed');
mockGetRaceDetails.mockReturnValue(viewModel);
mockGetMembership.mockReturnValue({ role: 'member' });
renderWithQueryClient(<RaceDetailInteractive />);
await waitFor(() => {
const tracks = screen.getAllByText('Test Track');
expect(tracks.length).toBeGreaterThan(0);
});
expect(screen.queryByText('Re-open Race')).toBeNull();
});
it('does not render Re-open Race button when race is not completed or cancelled even for admin', async () => {
mockIsOwnerOrAdmin.mockReturnValue(true);
const viewModel = createViewModel('scheduled');
mockGetRaceDetails.mockReturnValue(viewModel);
mockGetMembership.mockReturnValue({ role: 'owner' });
renderWithQueryClient(<RaceDetailInteractive />);
await waitFor(() => {
const tracks = screen.getAllByText('Test Track');
expect(tracks.length).toBeGreaterThan(0);
});
expect(screen.queryByText('Re-open Race')).toBeNull();
});
});