184 lines
5.3 KiB
TypeScript
184 lines
5.3 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 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: () => <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 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(<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(() => {
|
|
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(<RaceDetailPage />);
|
|
|
|
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(<RaceDetailPage />);
|
|
|
|
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(<RaceDetailPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(mockGetRaceDetails).toHaveBeenCalled();
|
|
});
|
|
|
|
expect(screen.queryByText('Re-open Race')).toBeNull();
|
|
});
|
|
});
|