Files
box-box/frontend/src/test/ChampionshipPage.test.tsx
AmanTahiliani 5cfaed30ba Add championship what-if simulator
Fourth view on the championship page: assign P1-P10 per remaining
round and see projected standings update live, with position deltas
and mathematically alive/eliminated title states. Pure projection
logic in lib/simulator.ts (2025 points system, standard GPs only);
scenarios persist to localStorage per season. 169 frontend tests
passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:31:09 -04:00

153 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RouterProvider, createRouter, createRootRoute, createRoute } from '@tanstack/react-router'
import { ChampionshipPage } from '../pages/ChampionshipPage'
import type { ChampHubDriver, ChampHubTeam, ChampionshipHub } from '../types'
vi.mock('../api', () => ({
fetchSeasons: vi.fn(),
fetchChampionshipHub: vi.fn(),
}))
import { fetchSeasons, fetchChampionshipHub } from '../api'
const mockFetchSeasons = vi.mocked(fetchSeasons)
const mockFetchHub = vi.mocked(fetchChampionshipHub)
function driver(over: Partial<ChampHubDriver>): ChampHubDriver {
return {
driver_number: 1,
name_acronym: 'VER',
full_name: 'Max Verstappen',
team_name: 'Red Bull',
team_colour: '3671c6',
points: 200,
position: 1,
wins: 5,
podiums: 8,
poles: 4,
form: [25, 18, 25, 15, 25],
cumulative: [25, 43, 68, 83, 108, 200],
teammate_wins: 9,
teammate_losses: 1,
...over,
}
}
const drivers: ChampHubDriver[] = [
driver({ driver_number: 1, name_acronym: 'VER', team_name: 'Red Bull', points: 200, position: 1 }),
driver({
driver_number: 4,
name_acronym: 'NOR',
full_name: 'Lando Norris',
team_name: 'McLaren',
team_colour: 'ff8000',
points: 160,
position: 2,
wins: 3,
cumulative: [18, 36, 54, 80, 120, 160],
}),
driver({
driver_number: 16,
name_acronym: 'LEC',
full_name: 'Charles Leclerc',
team_name: 'Ferrari',
team_colour: 'e8002d',
points: 120,
position: 3,
wins: 1,
cumulative: [15, 28, 40, 60, 90, 120],
}),
]
const teams: ChampHubTeam[] = [
{ team_name: 'Red Bull', team_colour: '3671c6', points: 260, position: 1, wins: 6 },
{ team_name: 'McLaren', team_colour: 'ff8000', points: 220, position: 2, wins: 3 },
{ team_name: 'Ferrari', team_colour: 'e8002d', points: 180, position: 3, wins: 1 },
]
const hub: ChampionshipHub = {
season: 2025,
round: 6,
total_rounds: 10,
rounds_left: 4,
last_race: 'Monaco GP',
round_labels: ['R1', 'R2', 'R3', 'R4', 'R5', 'R6'],
drivers,
teams,
}
function renderPage() {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const rootRoute = createRootRoute({
component: () => (
<QueryClientProvider client={queryClient}>
<ChampionshipPage />
</QueryClientProvider>
),
})
const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', component: ChampionshipPage })
const router = createRouter({ routeTree: rootRoute.addChildren([indexRoute]) })
return render(<RouterProvider router={router} />)
}
describe('ChampionshipPage', () => {
beforeEach(() => {
vi.clearAllMocks()
window.localStorage.clear()
mockFetchSeasons.mockResolvedValue([2025])
mockFetchHub.mockResolvedValue(hub)
})
it('renders the drivers view with leader and title math', async () => {
renderPage()
await waitFor(() => {
expect(screen.getByTestId('championship')).toBeInTheDocument()
})
expect(screen.getByTestId('champ-view-drivers')).toBeInTheDocument()
// Leader code shows in the stat rail and the table.
expect(screen.getAllByText('VER').length).toBeGreaterThan(0)
expect(screen.getByText('Monaco GP', { exact: false })).toBeInTheDocument()
expect(screen.getByTestId('champ-titlemath')).toHaveTextContent('mathematically win the title')
})
it('switches to constructors and progression views', async () => {
renderPage()
await waitFor(() => expect(screen.getByTestId('championship')).toBeInTheDocument())
fireEvent.click(screen.getByTestId('champ-tab-constructors'))
expect(screen.getByTestId('champ-view-constructors')).toBeInTheDocument()
expect(screen.getAllByText('Red Bull', { exact: false }).length).toBeGreaterThan(0)
fireEvent.click(screen.getByTestId('champ-tab-progression'))
expect(screen.getByTestId('champ-view-progression')).toBeInTheDocument()
expect(screen.getByText('Cumulative points', { exact: false })).toBeInTheDocument()
})
it('switches to the simulator view and projects standings', async () => {
renderPage()
await waitFor(() => expect(screen.getByTestId('championship')).toBeInTheDocument())
fireEvent.click(screen.getByTestId('champ-tab-simulator'))
expect(screen.getByTestId('champ-view-simulator')).toBeInTheDocument()
expect(screen.getByTestId('sim-projected')).toBeInTheDocument()
// 4 rounds left, default scenario: VER projects to 200 + 4×25 = 300.
expect(screen.getByText('300')).toBeInTheDocument()
})
it('shows the empty state when no drivers are returned', async () => {
mockFetchHub.mockResolvedValue({ ...hub, drivers: [], teams: [] })
renderPage()
await waitFor(() => {
expect(screen.getByTestId('championship-empty')).toBeInTheDocument()
})
expect(screen.getByText('No championship data')).toBeInTheDocument()
})
})