mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
feat(frontend): add next-race preview page (#25)
Assemble /preview from existing schedule, track-outline, results, and championship hub endpoints with client-side preview.ts helpers, section loading/empty states, and Vitest coverage for lib + page render paths. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
226
frontend/src/test/preview.test.ts
Normal file
226
frontend/src/test/preview.test.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import type { ChampHubDriver, ChampionshipHub, EnrichedGrid, EnrichedResult, Meeting, Session } from '../types'
|
||||
import {
|
||||
buildTitleFightContext,
|
||||
countdownTargetSession,
|
||||
extractPodium,
|
||||
extractPole,
|
||||
findPriorYearMeetingByCircuit,
|
||||
findRaceSession,
|
||||
formatPointsGap,
|
||||
isSprintWeekend,
|
||||
pickPreviewMeeting,
|
||||
RACE_WIN_POINTS,
|
||||
SPRINT_WEEKEND_MAX_POINTS,
|
||||
} from '../lib/preview'
|
||||
|
||||
const meeting = (overrides: Partial<Meeting> = {}): Meeting => ({
|
||||
meeting_key: 1,
|
||||
meeting_name: 'Monaco',
|
||||
meeting_official_name: 'Monaco GP',
|
||||
location: 'Monaco',
|
||||
country_name: 'Monaco',
|
||||
country_code: 'MON',
|
||||
country_flag: '',
|
||||
circuit_key: 10,
|
||||
circuit_short_name: 'Monaco',
|
||||
date_start: '2026-05-22T00:00:00+00:00',
|
||||
date_end: '2026-05-24T23:59:59+00:00',
|
||||
year: 2026,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const session = (overrides: Partial<Session> = {}): Session => ({
|
||||
session_key: 100,
|
||||
session_name: 'Race',
|
||||
session_type: 'Race',
|
||||
meeting_key: 1,
|
||||
date_start: '2026-05-24T13:00:00+00:00',
|
||||
date_end: '2026-05-24T15:00:00+00:00',
|
||||
gmt_offset: '02:00:00',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const hubDriver = (over: Partial<ChampHubDriver>): ChampHubDriver => ({
|
||||
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],
|
||||
cumulative: [200],
|
||||
teammate_wins: 9,
|
||||
teammate_losses: 1,
|
||||
...over,
|
||||
})
|
||||
|
||||
describe('preview lib', () => {
|
||||
it('picks the next upcoming meeting', () => {
|
||||
const now = new Date('2026-01-01T00:00:00Z')
|
||||
const meetings = [
|
||||
meeting({ meeting_key: 1, date_start: '2026-05-22T00:00:00+00:00' }),
|
||||
meeting({ meeting_key: 2, meeting_name: 'Spain', date_start: '2026-06-12T00:00:00+00:00' }),
|
||||
]
|
||||
expect(pickPreviewMeeting(meetings, now)?.meeting_key).toBe(1)
|
||||
})
|
||||
|
||||
it('returns null when no upcoming meeting exists', () => {
|
||||
const now = new Date('2027-01-01T00:00:00Z')
|
||||
const meetings = [meeting({ date_start: '2026-05-22T00:00:00+00:00' })]
|
||||
expect(pickPreviewMeeting(meetings, now)).toBeNull()
|
||||
})
|
||||
|
||||
it('skips cancelled meetings', () => {
|
||||
const now = new Date('2026-01-01T00:00:00Z')
|
||||
const meetings = [meeting({ is_cancelled: true })]
|
||||
expect(pickPreviewMeeting(meetings, now)).toBeNull()
|
||||
})
|
||||
|
||||
it('matches prior-year meeting by circuit_key', () => {
|
||||
const prior = [
|
||||
meeting({ meeting_key: 50, year: 2025, circuit_key: 10 }),
|
||||
meeting({ meeting_key: 51, year: 2025, circuit_key: 20, meeting_name: 'Spain' }),
|
||||
]
|
||||
expect(findPriorYearMeetingByCircuit(prior, 10)?.meeting_key).toBe(50)
|
||||
expect(findPriorYearMeetingByCircuit(prior, 99)).toBeNull()
|
||||
expect(findPriorYearMeetingByCircuit(prior, undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('finds the grand prix race session', () => {
|
||||
const sessions = [
|
||||
session({ session_key: 1, session_name: 'FP1', session_type: 'Practice', date_start: '2026-05-22T10:00:00+00:00' }),
|
||||
session({ session_key: 2, session_name: 'Sprint', session_type: 'Sprint', date_start: '2026-05-23T10:00:00+00:00' }),
|
||||
session({ session_key: 3, session_name: 'Race', session_type: 'Race', date_start: '2026-05-24T13:00:00+00:00' }),
|
||||
]
|
||||
expect(findRaceSession(sessions)?.session_key).toBe(3)
|
||||
})
|
||||
|
||||
it('detects sprint weekends', () => {
|
||||
const sprint = [
|
||||
session({ session_type: 'Practice' }),
|
||||
session({ session_name: 'Sprint', session_type: 'Sprint' }),
|
||||
session({ session_type: 'Race' }),
|
||||
]
|
||||
const normal = [session({ session_type: 'Practice' }), session({ session_type: 'Race' })]
|
||||
expect(isSprintWeekend(sprint)).toBe(true)
|
||||
expect(isSprintWeekend(normal)).toBe(false)
|
||||
})
|
||||
|
||||
it('extracts podium and pole', () => {
|
||||
const results: EnrichedResult[] = [
|
||||
{
|
||||
driver_number: 1,
|
||||
position: 1,
|
||||
name_acronym: 'VER',
|
||||
full_name: 'Max Verstappen',
|
||||
team_name: 'Red Bull',
|
||||
team_colour: '3671c6',
|
||||
dnf: false,
|
||||
dns: false,
|
||||
dsq: false,
|
||||
duration: 100,
|
||||
gap_to_leader: 0,
|
||||
number_of_laps: 78,
|
||||
points: 25,
|
||||
session_key: 1,
|
||||
meeting_key: 1,
|
||||
},
|
||||
{
|
||||
driver_number: 4,
|
||||
position: 2,
|
||||
name_acronym: 'NOR',
|
||||
full_name: 'Lando Norris',
|
||||
team_name: 'McLaren',
|
||||
team_colour: 'ff8000',
|
||||
dnf: false,
|
||||
dns: false,
|
||||
dsq: false,
|
||||
duration: 101,
|
||||
gap_to_leader: 1,
|
||||
number_of_laps: 78,
|
||||
points: 18,
|
||||
session_key: 1,
|
||||
meeting_key: 1,
|
||||
},
|
||||
{
|
||||
driver_number: 16,
|
||||
position: 3,
|
||||
name_acronym: 'LEC',
|
||||
full_name: 'Charles Leclerc',
|
||||
team_name: 'Ferrari',
|
||||
team_colour: 'e8002d',
|
||||
dnf: false,
|
||||
dns: false,
|
||||
dsq: false,
|
||||
duration: 102,
|
||||
gap_to_leader: 2,
|
||||
number_of_laps: 78,
|
||||
points: 15,
|
||||
session_key: 1,
|
||||
meeting_key: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const grid: EnrichedGrid[] = [
|
||||
{
|
||||
driver_number: 4,
|
||||
position: 1,
|
||||
name_acronym: 'NOR',
|
||||
full_name: 'Lando Norris',
|
||||
team_name: 'McLaren',
|
||||
team_colour: 'ff8000',
|
||||
session_key: 1,
|
||||
meeting_key: 1,
|
||||
lap_duration: 70,
|
||||
},
|
||||
]
|
||||
|
||||
const podium = extractPodium(results)
|
||||
expect(podium.map((p) => p.name_acronym)).toEqual(['VER', 'NOR', 'LEC'])
|
||||
expect(extractPole(grid)?.name_acronym).toBe('NOR')
|
||||
})
|
||||
|
||||
it('builds title fight gaps for top 3', () => {
|
||||
const hub: ChampionshipHub = {
|
||||
season: 2026,
|
||||
round: 5,
|
||||
total_rounds: 24,
|
||||
rounds_left: 19,
|
||||
last_race: 'Monaco GP',
|
||||
round_labels: ['R1'],
|
||||
drivers: [
|
||||
hubDriver({ points: 200, position: 1 }),
|
||||
hubDriver({ driver_number: 4, name_acronym: 'NOR', points: 160, position: 2 }),
|
||||
hubDriver({ driver_number: 16, name_acronym: 'LEC', points: 120, position: 3 }),
|
||||
],
|
||||
teams: [],
|
||||
}
|
||||
|
||||
const rows = buildTitleFightContext(hub)
|
||||
expect(rows).toHaveLength(3)
|
||||
expect(rows[0].gapToLeader).toBe(0)
|
||||
expect(rows[1].gapToLeader).toBe(40)
|
||||
expect(rows[1].gapAfterRaceWin).toBe(15)
|
||||
expect(rows[2].gapAfterSprintWeekendMax).toBe(200 - (120 + SPRINT_WEEKEND_MAX_POINTS))
|
||||
expect(RACE_WIN_POINTS).toBe(25)
|
||||
})
|
||||
|
||||
it('formats points gaps', () => {
|
||||
expect(formatPointsGap(0)).toBe('LEADER')
|
||||
expect(formatPointsGap(40)).toBe('+40')
|
||||
})
|
||||
|
||||
it('picks countdown target as next future session', () => {
|
||||
const sessions = [
|
||||
session({ session_key: 1, session_name: 'FP1', session_type: 'Practice', date_start: '2026-05-22T10:00:00+00:00', date_end: '2026-05-22T11:00:00+00:00' }),
|
||||
session({ session_key: 2, session_name: 'Race', session_type: 'Race', date_start: '2026-05-24T13:00:00+00:00', date_end: '2026-05-24T15:00:00+00:00' }),
|
||||
]
|
||||
const now = new Date('2026-05-23T12:00:00Z')
|
||||
expect(countdownTargetSession(sessions, now)?.session_key).toBe(2)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user