import React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import RaceDetailPage from './page'; import { RaceDetailViewModel } from '@/lib/view-models/RaceDetailViewModel'; // 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 mockGetRaceDetail = vi.fn(); const mockReopenRace = vi.fn(); const mockFetchLeagueMemberships = vi.fn(); const mockGetMembership = vi.fn(); vi.mock('@/lib/services/ServiceProvider', () => ({ useServices: () => ({ raceService: { getRaceDetail: mockGetRaceDetail, 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 createViewModel = (status: string) => { return new RaceDetailViewModel({ race: { id: 'race-123', track: 'Test Track', car: 'Test Car', scheduledAt: '2023-12-31T20:00:00Z', status, sessionType: 'race', strengthOfField: null, registeredCount: 0, maxParticipants: 32, } as any, league: { id: 'league-1', name: 'Test League', description: 'Test league description', settings: { maxDrivers: 32, qualifyingFormat: 'open', }, } as any, entryList: [], registration: { isRegistered: false, canRegister: false, } as any, userResult: null, }); }; describe('RaceDetailPage - Re-open Race behavior', () => { beforeEach(() => { mockGetRaceDetail.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 mockGetRaceDetail.mockResolvedValue(viewModel); const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); render(); const reopenButton = await screen.findByText('Re-open Race'); 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(mockGetRaceDetail).toHaveBeenCalled(); }); confirmSpy.mockRestore(); }); it('does not render Re-open Race button for non-admin viewer', async () => { mockIsOwnerOrAdmin.mockReturnValue(false); const viewModel = createViewModel('completed'); mockGetRaceDetail.mockResolvedValue(viewModel); render(); await waitFor(() => { expect(mockGetRaceDetail).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'); mockGetRaceDetail.mockResolvedValue(viewModel); render(); await waitFor(() => { expect(mockGetRaceDetail).toHaveBeenCalled(); }); expect(screen.queryByText('Re-open Race')).toBeNull(); }); });