Add Race Hub analytics visuals

This commit is contained in:
2026-05-25 02:19:53 -04:00
parent dfd0cf7c12
commit b5d87775a6
10 changed files with 826 additions and 108 deletions

View File

@@ -0,0 +1,200 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { PositionEvolutionView } from '../components/PositionEvolutionView'
import type { EnrichedResult, EnrichedGrid, PositionSample, Lap } from '../types'
const results: EnrichedResult[] = [
{
driver_number: 1,
position: 1,
name_acronym: 'VER',
full_name: 'Max Verstappen',
team_name: 'Red Bull Racing',
team_colour: '3671C6',
dnf: false,
dns: false,
dsq: false,
duration: null,
gap_to_leader: null,
number_of_laps: 78,
points: 25,
session_key: 9472,
meeting_key: 1229,
},
{
driver_number: 44,
position: 2,
name_acronym: 'HAM',
full_name: 'Lewis Hamilton',
team_name: 'Ferrari',
team_colour: 'E8002D',
dnf: false,
dns: false,
dsq: false,
duration: null,
gap_to_leader: 5.1,
number_of_laps: 78,
points: 18,
session_key: 9472,
meeting_key: 1229,
},
]
const grid: EnrichedGrid[] = [
{
driver_number: 1,
position: 1,
name_acronym: 'VER',
full_name: 'Max Verstappen',
team_name: 'Red Bull Racing',
team_colour: '3671C6',
session_key: 9472,
meeting_key: 1229,
lap_duration: 71.234,
},
{
driver_number: 44,
position: 2,
name_acronym: 'HAM',
full_name: 'Lewis Hamilton',
team_name: 'Ferrari',
team_colour: 'E8002D',
session_key: 9472,
meeting_key: 1229,
lap_duration: 71.456,
},
]
const positions: PositionSample[] = [
{
session_key: 9472,
driver_number: 1,
meeting_key: 1229,
date: '2025-05-25T13:05:00+00:00',
position: 1,
},
{
session_key: 9472,
driver_number: 1,
meeting_key: 1229,
date: '2025-05-25T13:10:00+00:00',
position: 1,
},
{
session_key: 9472,
driver_number: 44,
meeting_key: 1229,
date: '2025-05-25T13:05:00+00:00',
position: 2,
},
]
const laps: Lap[] = [
{
session_key: 9472,
driver_number: 1,
meeting_key: 1229,
lap_number: 1,
date_start: '2025-05-25T13:00:00+00:00',
lap_duration: 75.1,
is_pit_out_lap: false,
},
]
describe('PositionEvolutionView — positions available', () => {
it('renders the position chart container', () => {
const { container } = render(
<PositionEvolutionView
results={results}
grid={grid}
positions={positions}
laps={laps}
hasPositions={true}
/>
)
expect(container.querySelector('[data-testid="position-chart"]')).toBeInTheDocument()
})
it('renders an SVG chart', () => {
const { container } = render(
<PositionEvolutionView
results={results}
grid={grid}
positions={positions}
laps={laps}
hasPositions={true}
/>
)
expect(container.querySelector('svg')).toBeInTheDocument()
expect(container.querySelectorAll('polyline').length).toBeGreaterThan(0)
})
it('does not show the missing-data notice', () => {
render(
<PositionEvolutionView
results={results}
grid={grid}
positions={positions}
laps={laps}
hasPositions={true}
/>
)
expect(screen.queryByText(/Lap-by-lap positions not available/i)).not.toBeInTheDocument()
})
it('renders Grid → Finish table below chart', () => {
render(
<PositionEvolutionView
results={results}
grid={grid}
positions={positions}
laps={laps}
hasPositions={true}
/>
)
expect(screen.getByText('Grid → Finish')).toBeInTheDocument()
})
})
describe('PositionEvolutionView — positions missing', () => {
it('shows the missing-data notice', () => {
render(
<PositionEvolutionView
results={results}
grid={grid}
positions={[]}
laps={[]}
hasPositions={false}
/>
)
expect(screen.getByText(/Lap-by-lap positions not available/i)).toBeInTheDocument()
})
it('falls back to grid → finish table when both results and grid exist', () => {
render(
<PositionEvolutionView
results={results}
grid={grid}
positions={[]}
laps={[]}
hasPositions={false}
/>
)
expect(screen.getByText('Grid → Finish')).toBeInTheDocument()
expect(screen.getByText('VER')).toBeInTheDocument()
expect(screen.getByText('HAM')).toBeInTheDocument()
})
it('does not render the position chart', () => {
const { container } = render(
<PositionEvolutionView
results={results}
grid={grid}
positions={[]}
laps={[]}
hasPositions={false}
/>
)
expect(container.querySelector('[data-testid="position-chart"]')).not.toBeInTheDocument()
})
})

View File

@@ -0,0 +1,134 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { StrategyView } from '../components/StrategyView'
import type { EnrichedResult, Stint, PitStop } from '../types'
const results: EnrichedResult[] = [
{
driver_number: 1,
position: 1,
name_acronym: 'VER',
full_name: 'Max Verstappen',
team_name: 'Red Bull Racing',
team_colour: '3671C6',
dnf: false,
dns: false,
dsq: false,
duration: null,
gap_to_leader: null,
number_of_laps: 78,
points: 25,
session_key: 9472,
meeting_key: 1229,
},
{
driver_number: 44,
position: 2,
name_acronym: 'HAM',
full_name: 'Lewis Hamilton',
team_name: 'Ferrari',
team_colour: 'E8002D',
dnf: false,
dns: false,
dsq: false,
duration: null,
gap_to_leader: 5.1,
number_of_laps: 78,
points: 18,
session_key: 9472,
meeting_key: 1229,
},
]
const stints: Stint[] = [
{
session_key: 9472,
driver_number: 1,
meeting_key: 1229,
stint_number: 1,
compound: 'MEDIUM',
lap_start: 1,
lap_end: 30,
tyre_age_at_start: 0,
},
{
session_key: 9472,
driver_number: 44,
meeting_key: 1229,
stint_number: 1,
compound: 'SOFT',
lap_start: 1,
lap_end: 18,
tyre_age_at_start: 0,
},
]
const pitStops: PitStop[] = [
{
session_key: 9472,
driver_number: 44,
meeting_key: 1229,
lap_number: 19,
date: '2025-05-25T14:00:00+00:00',
pit_duration: 2.4,
lane_duration: 0,
stop_duration: 2.4,
},
]
describe('StrategyView — stints available', () => {
it('renders the strategy chart container', () => {
const { container } = render(
<StrategyView results={results} stints={stints} pit_stops={pitStops} hasStints={true} />
)
expect(container.querySelector('[data-testid="strategy-chart"]')).toBeInTheDocument()
})
it('renders driver acronyms as SVG text', () => {
render(
<StrategyView results={results} stints={stints} pit_stops={pitStops} hasStints={true} />
)
expect(screen.getByText('VER')).toBeInTheDocument()
expect(screen.getByText('HAM')).toBeInTheDocument()
})
it('renders an SVG stint chart', () => {
const { container } = render(
<StrategyView results={results} stints={stints} pit_stops={pitStops} hasStints={true} />
)
expect(container.querySelector('svg')).toBeInTheDocument()
expect(container.querySelectorAll('rect').length).toBeGreaterThan(0)
})
it('does not show the stints-unavailable notice', () => {
render(
<StrategyView results={results} stints={stints} pit_stops={pitStops} hasStints={true} />
)
expect(screen.queryByText(/Stints not available/i)).not.toBeInTheDocument()
})
})
describe('StrategyView — stints missing', () => {
it('shows the missing-data notice', () => {
render(
<StrategyView results={results} stints={[]} pit_stops={[]} hasStints={false} />
)
expect(screen.getByText(/Stints not available/i)).toBeInTheDocument()
})
it('falls back to laps-completed table', () => {
render(
<StrategyView results={results} stints={[]} pit_stops={[]} hasStints={false} />
)
expect(screen.getByText('VER')).toBeInTheDocument()
expect(screen.getByText('HAM')).toBeInTheDocument()
expect(screen.getAllByText('78').length).toBe(2)
})
it('does not render the strategy chart', () => {
const { container } = render(
<StrategyView results={results} stints={[]} pit_stops={[]} hasStints={false} />
)
expect(container.querySelector('[data-testid="strategy-chart"]')).not.toBeInTheDocument()
})
})