resolve todos in website
This commit is contained in:
169
apps/website/app/races/[id]/page.test.tsx
Normal file
169
apps/website/app/races/[id]/page.test.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
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: () => <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 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(<RaceDetailPage />);
|
||||
|
||||
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(<RaceDetailPage />);
|
||||
|
||||
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(<RaceDetailPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetRaceDetail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Re-open Race')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user