Complete React live and Race Hub views

This commit is contained in:
2026-05-25 02:56:06 -04:00
parent 5eb0983ffb
commit e410a01db1
23 changed files with 1243 additions and 12 deletions

View File

@@ -119,8 +119,8 @@ describe('DataLibraryPage', () => {
expect(mockFetchLocalMeetings).toHaveBeenCalledWith(2025)
})
expect(await screen.findByText('Monaco')).toBeInTheDocument()
expect(await screen.findByTestId('meeting-detail')).toBeInTheDocument()
expect(screen.getAllByText('Monaco').length).toBeGreaterThan(0)
expect(screen.getByText('11/11')).toBeInTheDocument()
expect(screen.getByText('box-box --ingest-meeting 1229')).toBeInTheDocument()
expect(screen.getByText('box-box --ingest-session 9472')).toBeInTheDocument()

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import { LapsView } from '../components/LapsView'
import type { Lap } from '../types'
const laps: Lap[] = [
{
session_key: 9472,
driver_number: 44,
meeting_key: 1229,
lap_number: 1,
date_start: '2025-05-25T13:04:00Z',
lap_duration: 75.2,
is_pit_out_lap: false,
},
{
session_key: 9472,
driver_number: 1,
meeting_key: 1229,
lap_number: 1,
date_start: '2025-05-25T13:04:01Z',
lap_duration: 72.1,
is_pit_out_lap: false,
},
{
session_key: 9472,
driver_number: 1,
meeting_key: 1229,
lap_number: 2,
date_start: '2025-05-25T13:05:14Z',
lap_duration: 73.5,
is_pit_out_lap: true,
},
]
describe('LapsView', () => {
it('renders compact best-lap rows by driver', () => {
render(<LapsView laps={laps} />)
expect(screen.getByTestId('laps-view')).toBeInTheDocument()
expect(screen.getByText('#1')).toBeInTheDocument()
expect(screen.getByText('#44')).toBeInTheDocument()
expect(screen.getByText('1:12.100')).toBeInTheDocument()
expect(screen.getByText('FASTEST')).toBeInTheDocument()
})
it('shows a missing-data state when no laps are present', () => {
render(<LapsView laps={[]} />)
expect(screen.getByText(/Laps not ingested/i)).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import { RaceControlView } from '../components/RaceControlView'
import type { RaceControlMessage } from '../types'
const messages: RaceControlMessage[] = [
{
session_key: 9472,
meeting_key: 1229,
date: '2025-05-25T13:10:00Z',
category: 'Flag',
flag: 'YELLOW',
message: 'Yellow flag in sector 2',
scope: 'Sector',
driver_number: null,
lap_number: 6,
sector: 2,
qualifying_phase: null,
},
{
session_key: 9472,
meeting_key: 1229,
date: '2025-05-25T13:12:00Z',
category: 'Other',
flag: '',
message: 'Car 44 noted for track limits',
scope: 'Driver',
driver_number: 44,
lap_number: 8,
sector: null,
qualifying_phase: null,
},
]
describe('RaceControlView', () => {
it('renders race-control messages from the payload array', () => {
render(<RaceControlView messages={messages} />)
expect(screen.getByTestId('race-control-view')).toBeInTheDocument()
expect(screen.getByText('YELLOW')).toBeInTheDocument()
expect(screen.getByText('Yellow flag in sector 2')).toBeInTheDocument()
expect(screen.getByText('Car 44 noted for track limits')).toBeInTheDocument()
})
it('shows a missing-data state when no messages are present', () => {
render(<RaceControlView messages={[]} />)
expect(screen.getByText(/Race control messages not ingested/i)).toBeInTheDocument()
})
})

View File

@@ -3,12 +3,15 @@ import { render, screen, fireEvent } from '@testing-library/react'
import { TabBar } from '../components/TabBar'
describe('TabBar', () => {
it('renders all 5 tabs', () => {
it('renders all Race Hub tabs', () => {
render(<TabBar active="results" onChange={() => {}} />)
expect(screen.getByRole('tab', { name: 'Results' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Grid' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Strategy' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Positions' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Laps' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Race Control' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Weather' })).toBeInTheDocument()
expect(screen.getByRole('tab', { name: 'Datasets' })).toBeInTheDocument()
})
@@ -29,7 +32,7 @@ describe('TabBar', () => {
it('calls onChange with the correct tab id when clicked', () => {
const onChange = vi.fn()
render(<TabBar active="results" onChange={onChange} />)
fireEvent.click(screen.getByRole('tab', { name: 'Datasets' }))
expect(onChange).toHaveBeenCalledWith('datasets')
fireEvent.click(screen.getByRole('tab', { name: 'Race Control' }))
expect(onChange).toHaveBeenCalledWith('race_control')
})
})

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest'
import { render, screen } from '@testing-library/react'
import { WeatherView } from '../components/WeatherView'
import type { WeatherSample } from '../types'
const weather: WeatherSample[] = [
{
session_key: 9472,
meeting_key: 1229,
date: '2025-05-25T13:00:00Z',
air_temperature: 20,
track_temperature: 30,
humidity: 60,
pressure: 1010,
rainfall: 0,
wind_direction: 180,
wind_speed: 2,
},
{
session_key: 9472,
meeting_key: 1229,
date: '2025-05-25T13:05:00Z',
air_temperature: 21,
track_temperature: 33,
humidity: 62,
pressure: 1011,
rainfall: 0.2,
wind_direction: 190,
wind_speed: 3,
},
]
describe('WeatherView', () => {
it('renders weather summary and recent samples', () => {
render(<WeatherView weather={weather} />)
expect(screen.getByTestId('weather-view')).toBeInTheDocument()
expect(screen.getByText('Avg air / track')).toBeInTheDocument()
expect(screen.getByText('20.5C / 31.5C')).toBeInTheDocument()
expect(screen.getByText('Rain samples')).toBeInTheDocument()
expect(screen.getByText('0.2')).toBeInTheDocument()
})
it('shows a missing-data state when no weather samples are present', () => {
render(<WeatherView weather={[]} />)
expect(screen.getByText(/Weather samples not ingested/i)).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,127 @@
import { describe, expect, it } from 'vitest'
import {
extrapolateClock,
latestRaceControl,
parseLiveStateEvent,
sortLiveTimingRows,
trackStatusLabel,
tyreClass,
tyreLabel,
} from '../lib/live'
import type { LiveStreamData } from '../types'
const snapshot: LiveStreamData = {
Drivers: {
'16': {
RacingNumber: '16',
Position: 1,
PrevPosition: 2,
GapToLeader: '',
Interval: '',
LastLapTime: '1:14.100',
LastLapPB: true,
LastLapOB: false,
BestLapTime: '1:13.900',
BestLapPB: false,
BestLapOB: false,
BestLapNum: 20,
InPit: false,
PitOut: false,
Retired: false,
KnockedOut: false,
Cutoff: false,
OnFlyingLap: false,
NumberOfLaps: 21,
SpeedTrap: '',
Sectors: [],
},
'1': {
RacingNumber: '1',
Position: 2,
PrevPosition: 1,
GapToLeader: '+1.200',
Interval: '+1.200',
LastLapTime: '1:14.300',
LastLapPB: false,
LastLapOB: false,
BestLapTime: '1:13.800',
BestLapPB: false,
BestLapOB: true,
BestLapNum: 19,
InPit: false,
PitOut: false,
Retired: false,
KnockedOut: false,
Cutoff: false,
OnFlyingLap: false,
NumberOfLaps: 21,
SpeedTrap: '',
Sectors: [],
},
},
DriverInfo: {
'16': {
RacingNumber: '16',
BroadcastName: 'C LECLERC',
Tla: 'LEC',
TeamName: 'Ferrari',
TeamColour: 'e8002d',
FirstName: 'Charles',
LastName: 'Leclerc',
},
'44': {
RacingNumber: '44',
BroadcastName: 'L HAMILTON',
Tla: 'HAM',
TeamName: 'Ferrari',
TeamColour: 'e8002d',
FirstName: 'Lewis',
LastName: 'Hamilton',
},
},
Tyres: {
'16': { Compound: 'MEDIUM', New: false, Age: 8 },
},
RCMessages: [
{ Time: '14:01', Category: 'Flag', Flag: 'GREEN', Message: 'GREEN LIGHT', Lap: 0 },
{ Time: '14:08', Category: 'Drs', Flag: '', Message: 'DRS ENABLED', Lap: 3 },
],
Weather: { AirTemp: 20, TrackTemp: 31, Humidity: 55, WindSpeed: 2, WindDir: 180, Rainfall: false },
Session: { MeetingName: 'Monaco Grand Prix', CircuitName: 'Monaco', SessionType: 'Race', SessionName: 'Race' },
TrackStatus: '1',
CurrentLap: 21,
TotalLaps: 78,
Clock: '01:20:00',
ClockRefTime: '2026-05-25T12:00:00Z',
ClockExtrapolating: true,
Stints: {},
}
describe('live transforms', () => {
it('parses live EventSource snapshots without changing PascalCase data', () => {
const parsed = parseLiveStateEvent(JSON.stringify({ is_live: true, data: snapshot }))
expect(parsed?.is_live).toBe(true)
expect(parsed?.data?.Drivers['16'].RacingNumber).toBe('16')
})
it('sorts timing rows by live position and includes drivers with metadata only', () => {
const rows = sortLiveTimingRows(snapshot)
expect(rows.map((row) => row.RacingNumber)).toEqual(['16', '1', '44'])
expect(rows[2].Position).toBe(3)
})
it('formats tyre labels and classes', () => {
expect(tyreLabel({ Compound: 'MEDIUM', New: false, Age: 8 })).toBe('M +8')
expect(tyreClass({ Compound: 'INTERMEDIATE', New: true, Age: 1 })).toBe('tyre-inter')
expect(tyreLabel(undefined)).toBe('?')
})
it('maps track status and race control ordering', () => {
expect(trackStatusLabel('4')).toBe('SC')
expect(latestRaceControl(snapshot.RCMessages, 1)[0].Message).toBe('DRS ENABLED')
})
it('extrapolates the session clock from the reference time', () => {
expect(extrapolateClock('01:20:00', '2026-05-25T12:00:00Z', true, Date.parse('2026-05-25T12:00:30Z'))).toBe('01:19:30')
})
})