mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
Complete React live and Race Hub views
This commit is contained in:
@@ -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()
|
||||
|
||||
52
frontend/src/test/LapsView.test.tsx
Normal file
52
frontend/src/test/LapsView.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
50
frontend/src/test/RaceControlView.test.tsx
Normal file
50
frontend/src/test/RaceControlView.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
49
frontend/src/test/WeatherView.test.tsx
Normal file
49
frontend/src/test/WeatherView.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
127
frontend/src/test/live.test.ts
Normal file
127
frontend/src/test/live.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user