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 RaceDetailPage from './page'; 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: () =>
, 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 services hook to provide raceService and leagueMembershipService const mockGetRaceDetails = vi.fn(); const mockReopenRace = vi.fn(); const mockFetchLeagueMemberships = vi.fn(); const mockGetMembership = vi.fn(); vi.mock('@/lib/services/ServiceProvider', () => ({ useServices: () => ({ raceService: { getRaceDetails: mockGetRaceDetails, reopenRace: mockReopenRace, // other methods are not used in this test }, leagueMembershipService: { fetchLeagueMemberships: mockFetchLeagueMemberships, getMembership: mockGetMembership, }, }), })); // 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({ui}); }; 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(() => { mockGetRaceDetails.mockReset(); mockReopenRace.mockReset(); mockFetchLeagueMemberships.mockReset(); mockGetMembership.mockReset(); mockIsOwnerOrAdmin.mockReset(); mockFetchLeagueMemberships.mockResolvedValue(undefined); mockGetMembership.mockReturnValue(null); }); 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'); // First call: initial load, second call: after re-open mockGetRaceDetails.mockResolvedValue(viewModel); const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); renderWithQueryClient(); const reopenButtons = await screen.findAllByText('Re-open Race'); const reopenButton = reopenButtons[0]!; expect(reopenButton).toBeInTheDocument(); mockReopenRace.mockResolvedValue(undefined); fireEvent.click(reopenButton); await waitFor(() => { expect(mockReopenRace).toHaveBeenCalledWith('race-123'); }); // loadRaceData should be called again after reopening await waitFor(() => { expect(mockGetRaceDetails).toHaveBeenCalled(); }); confirmSpy.mockRestore(); }); it('does not render Re-open Race button for non-admin viewer', async () => { mockIsOwnerOrAdmin.mockReturnValue(false); const viewModel = createViewModel('completed'); mockGetRaceDetails.mockResolvedValue(viewModel); renderWithQueryClient(); await waitFor(() => { expect(mockGetRaceDetails).toHaveBeenCalled(); }); 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.mockResolvedValue(viewModel); renderWithQueryClient(); await waitFor(() => { expect(mockGetRaceDetails).toHaveBeenCalled(); }); expect(screen.queryByText('Re-open Race')).toBeNull(); }); });