wip league admin tools

This commit is contained in:
2025-12-28 12:04:12 +01:00
parent 5dc8c2399c
commit 6edf12fda8
401 changed files with 15365 additions and 6047 deletions

View File

@@ -5,7 +5,7 @@ import '@testing-library/jest-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import RaceDetailPage from './page';
import { RaceDetailViewModel } from '@/lib/view-models/RaceDetailViewModel';
import type { RaceDetailsViewModel } from '@/lib/view-models/RaceDetailsViewModel';
// Mocks for Next.js navigation
const mockPush = vi.fn();
@@ -40,7 +40,7 @@ vi.mock('@/components/sponsors/SponsorInsightsCard', () => ({
}));
// Mock services hook to provide raceService and leagueMembershipService
const mockGetRaceDetail = vi.fn();
const mockGetRaceDetails = vi.fn();
const mockReopenRace = vi.fn();
const mockFetchLeagueMemberships = vi.fn();
const mockGetMembership = vi.fn();
@@ -48,7 +48,7 @@ const mockGetMembership = vi.fn();
vi.mock('@/lib/services/ServiceProvider', () => ({
useServices: () => ({
raceService: {
getRaceDetail: mockGetRaceDetail,
getRaceDetails: mockGetRaceDetails,
reopenRace: mockReopenRace,
// other methods are not used in this test
},
@@ -79,8 +79,10 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
};
const createViewModel = (status: string) => {
return new RaceDetailViewModel({
const createViewModel = (status: string): RaceDetailsViewModel => {
const canReopenRace = status === 'completed' || status === 'cancelled';
return {
race: {
id: 'race-123',
track: 'Test Track',
@@ -88,10 +90,7 @@ const createViewModel = (status: string) => {
scheduledAt: '2023-12-31T20:00:00Z',
status,
sessionType: 'race',
strengthOfField: null,
registeredCount: 0,
maxParticipants: 32,
} as any,
},
league: {
id: 'league-1',
name: 'Test League',
@@ -100,19 +99,20 @@ const createViewModel = (status: string) => {
maxDrivers: 32,
qualifyingFormat: 'open',
},
} as any,
},
entryList: [],
registration: {
isRegistered: false,
isUserRegistered: false,
canRegister: false,
} as any,
},
userResult: null,
}, 'driver-1');
canReopenRace,
};
};
describe('RaceDetailPage - Re-open Race behavior', () => {
beforeEach(() => {
mockGetRaceDetail.mockReset();
mockGetRaceDetails.mockReset();
mockReopenRace.mockReset();
mockFetchLeagueMemberships.mockReset();
mockGetMembership.mockReset();
@@ -127,7 +127,7 @@ describe('RaceDetailPage - Re-open Race behavior', () => {
const viewModel = createViewModel('completed');
// First call: initial load, second call: after re-open
mockGetRaceDetail.mockResolvedValue(viewModel);
mockGetRaceDetails.mockResolvedValue(viewModel);
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
@@ -147,7 +147,7 @@ describe('RaceDetailPage - Re-open Race behavior', () => {
// loadRaceData should be called again after reopening
await waitFor(() => {
expect(mockGetRaceDetail).toHaveBeenCalled();
expect(mockGetRaceDetails).toHaveBeenCalled();
});
confirmSpy.mockRestore();
@@ -156,12 +156,12 @@ describe('RaceDetailPage - Re-open Race behavior', () => {
it('does not render Re-open Race button for non-admin viewer', async () => {
mockIsOwnerOrAdmin.mockReturnValue(false);
const viewModel = createViewModel('completed');
mockGetRaceDetail.mockResolvedValue(viewModel);
mockGetRaceDetails.mockResolvedValue(viewModel);
renderWithQueryClient(<RaceDetailPage />);
await waitFor(() => {
expect(mockGetRaceDetail).toHaveBeenCalled();
expect(mockGetRaceDetails).toHaveBeenCalled();
});
expect(screen.queryByText('Re-open Race')).toBeNull();
@@ -170,12 +170,12 @@ describe('RaceDetailPage - Re-open Race behavior', () => {
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);
mockGetRaceDetails.mockResolvedValue(viewModel);
renderWithQueryClient(<RaceDetailPage />);
await waitFor(() => {
expect(mockGetRaceDetail).toHaveBeenCalled();
expect(mockGetRaceDetails).toHaveBeenCalled();
});
expect(screen.queryByText('Re-open Race')).toBeNull();

View File

@@ -278,7 +278,7 @@ export default function RaceDetailPage() {
const entryList: RaceDetailEntryViewModel[] = viewModel.entryList;
const registration = viewModel.registration;
const userResult: RaceDetailUserResultViewModel | null = viewModel.userResult;
const raceSOF = null; // TODO: Add strengthOfField to RaceDetailRaceDTO
const raceSOF = null; // TODO: Add strength of field to race details response
const config = statusConfig[race.status as keyof typeof statusConfig];
const StatusIcon = config.icon;
@@ -636,7 +636,7 @@ export default function RaceDetailPage() {
{raceSOF ?? '—'}
</p>
</div>
{/* TODO: Add registeredCount and maxParticipants to RaceDetailRaceDTO */}
{/* TODO: Add registered count and max participants to race details response */}
{/* {race.registeredCount !== undefined && (
<div className="p-4 bg-deep-graphite rounded-lg">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">Registered</p>