diff --git a/frontend/src/components/ClassificationTable.tsx b/frontend/src/components/ClassificationTable.tsx
deleted file mode 100644
index 7312d54..0000000
--- a/frontend/src/components/ClassificationTable.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import type { EnrichedResult, EnrichedGrid } from '../types'
-import { DriverCell } from './DriverCell'
-import { formatDuration, formatGap, positionClass, gridDelta, gridDeltaClass } from '../utils'
-
-interface Props {
- results: EnrichedResult[]
- grid: EnrichedGrid[]
-}
-
-export function ClassificationTable({ results, grid }: Props) {
- if (results.length === 0) {
- return (
-
- Results not ingested. Run{' '}
- box-box --ingest-session <key> to load this dataset.
-
- )
- }
-
- const gridByDriver = Object.fromEntries(grid.map((g) => [g.driver_number, g.position]))
-
- const isRace = results.some((r) => r.points > 0 || r.number_of_laps > 0)
-
- return (
-
-
-
-
- P
- Driver
- Team
- {isRace && Grid }
- {isRace && Δ }
- Time / Gap
- {isRace && Pts }
-
-
-
- {results.map((r) => {
- const gridPos = gridByDriver[r.driver_number] ?? 0
- const timeStr = r.dnf
- ? null
- : r.dns
- ? null
- : r.dsq
- ? null
- : r.position === 1
- ? formatDuration(r.duration)
- : formatGap(r.gap_to_leader)
-
- return (
-
-
- {r.position}
-
-
-
-
-
- {r.team_name}
-
- {isRace && (
-
- {gridPos || '—'}
-
- )}
- {isRace && (
-
-
- {gridDelta(r.position, gridPos)}
-
-
- )}
-
- {r.dnf && DNF }
- {r.dns && DNS }
- {r.dsq && DSQ }
- {!r.dnf && !r.dns && !r.dsq && (
- {timeStr ?? '—'}
- )}
-
- {isRace && (
- 0 ? 700 : 400, color: r.points > 0 ? 'var(--text)' : 'var(--text-3)' }}>
- {r.points > 0 ? r.points : '—'}
-
- )}
-
- )
- })}
-
-
-
- )
-}
diff --git a/frontend/src/components/PositionEvolutionView.tsx b/frontend/src/components/PositionEvolutionView.tsx
deleted file mode 100644
index bcd7b10..0000000
--- a/frontend/src/components/PositionEvolutionView.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-import type { EnrichedResult, EnrichedGrid, PositionSample, Lap } from '../types'
-import { gridDelta, gridDeltaClass } from '../utils'
-
-interface Props {
- results: EnrichedResult[]
- grid: EnrichedGrid[]
- positions: PositionSample[]
- laps: Lap[]
- hasPositions: boolean
-}
-
-export function PositionEvolutionView({ results, grid, positions, laps: _laps, hasPositions }: Props) {
- if (!hasPositions) {
- return (
-
-
- Lap-by-lap positions not available. This session does not
- have ingested position samples in /api/v1/race-hub. Evolution
- charts require per-driver position samples over time.
-
-
- {results.length > 0 && grid.length > 0 && (
- <>
-
- Grid → Finish
- net positions gained/lost
-
-
-
-
- Driver
- Team
- Grid
- Finish
- Δ
-
-
-
- {results.map((r) => {
- const gridPos =
- grid.find((g) => g.driver_number === r.driver_number)?.position ?? 0
- return (
-
-
-
- {r.name_acronym || r.driver_number}
-
-
-
- {r.team_name}
-
-
- {gridPos || '—'}
-
- {r.position}
-
-
- {gridDelta(r.position, gridPos)}
-
-
-
- )
- })}
-
-
- >
- )}
-
- )
- }
-
- // Build time-indexed position series per driver
- const allTimes = [...new Set(positions.map((p) => p.date))].sort()
-
- if (allTimes.length === 0) {
- return No position samples in this dataset.
- }
-
- const tMin = new Date(allTimes[0]).getTime()
- const tMax = new Date(allTimes[allTimes.length - 1]).getTime()
- const tRange = Math.max(tMax - tMin, 1)
-
- const byDriver = new Map>()
- for (const p of positions) {
- if (!byDriver.has(p.driver_number)) byDriver.set(p.driver_number, [])
- byDriver.get(p.driver_number)!.push({
- t: (new Date(p.date).getTime() - tMin) / tRange,
- pos: p.position,
- })
- }
- for (const samples of byDriver.values()) {
- samples.sort((a, b) => a.t - b.t)
- }
-
- const maxPos = Math.max(...positions.map((p) => p.position), results.length, 2)
- const colorByDriver = new Map(results.map((r) => [r.driver_number, r.team_colour]))
- const acronymByDriver = new Map(results.map((r) => [r.driver_number, r.name_acronym]))
-
- const W = 640
- const H = 180
- const PL = 40
- const PR = 48 // right margin for driver labels
- const PT = 8
- const PB = 8
- const plotW = W - PL - PR
- const plotH = H - PT - PB
-
- const toX = (t: number) => PL + t * plotW
- const toY = (pos: number) => PT + ((pos - 1) / Math.max(maxPos - 1, 1)) * plotH
-
- return (
-
-
-
- {/* Horizontal grid lines + P# labels */}
- {Array.from({ length: maxPos }, (_, i) => i + 1).map((pos) => (
-
-
-
- P{pos}
-
-
- ))}
-
- {/* Driver lines */}
- {Array.from(byDriver.entries()).map(([dNum, samples]) => {
- const colour = colorByDriver.get(dNum)
- const color = colour ? `#${colour}` : '#888'
- const pts = samples.map((s) => `${toX(s.t)},${toY(s.pos)}`).join(' ')
- const last = samples[samples.length - 1]
-
- return (
-
-
- {samples.map((s, i) => (
-
- ))}
- {last && (
-
- {acronymByDriver.get(dNum) ?? dNum}
-
- )}
-
- )
- })}
-
-
-
- {/* Grid → Finish table below chart for context */}
- {results.length > 0 && grid.length > 0 && (
- <>
-
- Grid → Finish
- net positions
-
-
-
-
- Driver
- Grid
- Finish
- Δ
-
-
-
- {results.map((r) => {
- const gridPos =
- grid.find((g) => g.driver_number === r.driver_number)?.position ?? 0
- return (
-
-
-
- {r.name_acronym || r.driver_number}
-
-
-
- {gridPos || '—'}
-
- {r.position}
-
-
- {gridDelta(r.position, gridPos)}
-
-
-
- )
- })}
-
-
- >
- )}
-
- )
-}
diff --git a/frontend/src/components/RaceStoryCanvas.tsx b/frontend/src/components/RaceStoryCanvas.tsx
new file mode 100644
index 0000000..e40ba23
--- /dev/null
+++ b/frontend/src/components/RaceStoryCanvas.tsx
@@ -0,0 +1,189 @@
+import type { EnrichedResult, EnrichedGrid, PositionSample, Lap } from '../types'
+import { gridDelta, gridDeltaClass, formatDuration, formatGap } from '../utils'
+
+interface Props {
+ data: {
+ results: EnrichedResult[]
+ starting_grid: EnrichedGrid[]
+ positions: PositionSample[]
+ laps: Lap[]
+ datasets: Record
+ }
+}
+
+export function RaceStoryCanvas({ data }: Props) {
+ const { results, starting_grid: grid, positions, datasets } = data
+ const hasPositions = datasets['positions']?.status === 'available'
+
+ // Position Evolution Chart Logic
+ const allTimes = [...new Set(positions.map((p) => p.date))].sort()
+ const hasChartData = hasPositions && allTimes.length > 0
+
+ let chartContent = null
+ if (hasChartData) {
+ const tMin = new Date(allTimes[0]).getTime()
+ const tMax = new Date(allTimes[allTimes.length - 1]).getTime()
+ const tRange = Math.max(tMax - tMin, 1)
+
+ const byDriver = new Map>()
+ for (const p of positions) {
+ if (!byDriver.has(p.driver_number)) byDriver.set(p.driver_number, [])
+ byDriver.get(p.driver_number)!.push({
+ t: (new Date(p.date).getTime() - tMin) / tRange,
+ pos: p.position,
+ })
+ }
+ for (const samples of byDriver.values()) {
+ samples.sort((a, b) => a.t - b.t)
+ }
+
+ const maxPos = Math.max(...positions.map((p) => p.position), results.length, 2)
+ const colorByDriver = new Map(results.map((r) => [r.driver_number, r.team_colour]))
+ const acronymByDriver = new Map(results.map((r) => [r.driver_number, r.name_acronym]))
+
+ const W = 640
+ const H = 180
+ const PL = 40
+ const PR = 48
+ const PT = 8
+ const PB = 8
+ const plotW = W - PL - PR
+ const plotH = H - PT - PB
+
+ const toX = (t: number) => PL + t * plotW
+ const toY = (pos: number) => PT + ((pos - 1) / Math.max(maxPos - 1, 1)) * plotH
+
+ chartContent = (
+
+
+ {Array.from({ length: maxPos }, (_, i) => i + 1).map((pos) => (
+
+
+
+ P{pos}
+
+
+ ))}
+
+ {Array.from(byDriver.entries()).map(([dNum, samples]) => {
+ const colour = colorByDriver.get(dNum)
+ const color = colour ? `#${colour}` : '#888'
+ const pts = samples.map((s) => `${toX(s.t)},${toY(s.pos)}`).join(' ')
+ const last = samples[samples.length - 1]
+
+ return (
+
+
+ {samples.map((s, i) => (
+
+ ))}
+ {last && (
+
+ {acronymByDriver.get(dNum) ?? dNum}
+
+ )}
+
+ )
+ })}
+
+
+ )
+ }
+
+ return (
+
+ {hasChartData ? (
+ chartContent
+ ) : (
+
+ Lap-by-lap positions not available. This session does not
+ have ingested position samples in /api/v1/race-hub.
+
+ )}
+
+ {results.length > 0 && (
+
+ {results.map((r, i) => {
+ const gridPos = grid.find((g) => g.driver_number === r.driver_number)?.position ?? 0
+ const isWinner = i === 0 && r.position === 1
+ const pClass = r.position === 1 ? 'rs-pos-p1' : r.position === 2 ? 'rs-pos-p2' : r.position === 3 ? 'rs-pos-p3' : ''
+
+ return (
+
+
+
{r.position}
+
+
+
+ {r.name_acronym || r.driver_number}
+ {r.team_name}
+
+
+
+
+
+
+
+
+ {gridDelta(r.position, gridPos)}
+
+
+ Grid
+
+
+
+ {isWinner ? formatDuration(r.duration) : formatGap(r.gap_to_leader)}
+ {isWinner ? 'Time' : 'Gap'}
+
+
+
+ 0 ? 'var(--text)' : 'var(--text-3)' }}>
+ {r.points}
+
+ Pts
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/components/StartingGridTable.tsx b/frontend/src/components/StartingGridTable.tsx
deleted file mode 100644
index 100f4ff..0000000
--- a/frontend/src/components/StartingGridTable.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import type { EnrichedGrid } from '../types'
-import { DriverCell } from './DriverCell'
-import { formatLapTime } from '../utils'
-
-interface Props {
- grid: EnrichedGrid[]
-}
-
-export function StartingGridTable({ grid }: Props) {
- if (grid.length === 0) {
- return (
-
- Starting grid not ingested. Run{' '}
- box-box --ingest-session <key> to load this dataset.
-
- )
- }
-
- return (
-
-
-
-
- P
- Driver
- Team
- Lap Time
-
-
-
- {grid.map((g) => (
-
-
-
- {g.position}
-
-
-
-
-
-
- {g.team_name}
-
-
- {g.lap_duration != null ? formatLapTime(g.lap_duration) : '—'}
-
-
- ))}
-
-
-
- )
-}
diff --git a/frontend/src/pages/RaceHubPage.tsx b/frontend/src/pages/RaceHubPage.tsx
index 2b72d09..5b37310 100644
--- a/frontend/src/pages/RaceHubPage.tsx
+++ b/frontend/src/pages/RaceHubPage.tsx
@@ -8,12 +8,10 @@ import {
fetchWeekend,
} from '../api'
import { DatasetStrip } from '../components/DatasetStrip'
-import { ClassificationTable } from '../components/ClassificationTable'
-import { StartingGridTable } from '../components/StartingGridTable'
+import { RaceStoryCanvas } from '../components/RaceStoryCanvas'
import { TabBar, type Tab } from '../components/TabBar'
import { DatasetStatusView } from '../components/DatasetStatusView'
import { StrategyView } from '../components/StrategyView'
-import { PositionEvolutionView } from '../components/PositionEvolutionView'
import { LapsView } from '../components/LapsView'
import { RaceControlView } from '../components/RaceControlView'
import { WeatherView } from '../components/WeatherView'
@@ -33,8 +31,6 @@ interface Props {
sessionKey: number
}
-type RaceStorySubview = 'classification' | 'grid' | 'positions'
-
function pickAnalysisSession(weekend: Weekend | undefined): WeekendSession | undefined {
if (!weekend) return undefined
const local = weekend.sessions.filter((s) => s.source === 'local')
@@ -50,7 +46,6 @@ function pickAnalysisSession(weekend: Weekend | undefined): WeekendSession | und
export function RaceHubPage({ sessionKey }: Props) {
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState('overview')
- const [storyView, setStoryView] = useState('classification')
const [switcherOpen, setSwitcherOpen] = useState(false)
// ─── Auto-redirect when no session_key is supplied ───
@@ -294,74 +289,7 @@ export function RaceHubPage({ sessionKey }: Props) {
{activeTab === 'race_story' && (
-
- setStoryView('classification')}
- >
- Classification
-
- setStoryView('grid')}
- >
- Starting Grid
-
- setStoryView('positions')}
- >
- Positions
-
-
-
- {storyView === 'classification' && (
- <>
-
- Final Classification
- {data.results.length > 0 && (
- {data.results.length} drivers
- )}
-
-
- >
- )}
-
- {storyView === 'grid' && (
- <>
-
- Starting Grid
- {data.starting_grid.length > 0 && (
- {data.starting_grid.length} positions
- )}
-
-
- >
- )}
-
- {storyView === 'positions' && (
- <>
-
- Position Evolution
-
-
- >
- )}
+
)}
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css
index 48312bf..06cbea8 100644
--- a/frontend/src/styles/app.css
+++ b/frontend/src/styles/app.css
@@ -2370,3 +2370,131 @@ a { color: inherit; text-decoration: none; }
text-transform: uppercase;
letter-spacing: 0.05em;
}
+
+/* ── Race Story Canvas ── */
+.race-story-canvas {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s6);
+}
+
+.rs-chart-container {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--s2);
+ padding: var(--s4) 0;
+ overflow-x: auto;
+}
+
+.rs-field-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--s1);
+}
+
+.rs-driver-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: var(--surface);
+ padding: var(--s3) var(--s4);
+ border: 1px solid var(--border);
+ border-radius: var(--s1);
+ transition: background 0.15s, border-color 0.15s;
+}
+
+.rs-driver-row:hover {
+ background: var(--surface-h);
+ border-color: var(--border-2);
+}
+
+.rs-driver-left {
+ display: flex;
+ align-items: center;
+ gap: var(--s5);
+ flex: 1;
+}
+
+.rs-pos-col {
+ width: 24px;
+ text-align: right;
+ font-family: var(--f-mono);
+ font-weight: 600;
+ color: var(--text-2);
+}
+
+.rs-pos-p1 { color: var(--yellow); }
+.rs-pos-p2 { color: var(--text); }
+.rs-pos-p3 { color: var(--text); }
+
+.rs-driver-cell {
+ display: flex;
+ align-items: center;
+ gap: var(--s4);
+ flex: 1;
+ min-width: 0;
+}
+
+.rs-driver-color {
+ width: 4px;
+ height: 20px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.rs-driver-identity {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.2;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.rs-driver-name {
+ font-family: var(--f-mono);
+ font-weight: 600;
+ color: var(--text);
+ font-size: 13px;
+}
+
+.rs-driver-team {
+ font-size: 11px;
+ color: var(--text-2);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.rs-driver-right {
+ display: flex;
+ align-items: center;
+ gap: var(--s6);
+ text-align: right;
+}
+
+.rs-metric {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ font-family: var(--f-mono);
+ font-size: 13px;
+ line-height: 1.2;
+}
+
+.rs-metric-label {
+ font-family: var(--f-ui);
+ font-size: 10px;
+ color: var(--text-3);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+@media (max-width: 640px) {
+ .rs-driver-right {
+ gap: var(--s4);
+ }
+ .rs-driver-team {
+ display: none;
+ }
+}
diff --git a/frontend/src/test/ClassificationTable.test.tsx b/frontend/src/test/ClassificationTable.test.tsx
deleted file mode 100644
index 6a258f8..0000000
--- a/frontend/src/test/ClassificationTable.test.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import { render, screen } from '@testing-library/react'
-import { ClassificationTable } from '../components/ClassificationTable'
-import type { EnrichedResult, EnrichedGrid } from '../types'
-
-const mockResults: EnrichedResult[] = [
- {
- driver_number: 16,
- position: 1,
- name_acronym: 'LEC',
- full_name: 'Charles Leclerc',
- team_name: 'Ferrari',
- team_colour: 'e8002d',
- dnf: false,
- dns: false,
- dsq: false,
- duration: 5534.456,
- gap_to_leader: null,
- number_of_laps: 78,
- points: 25,
- session_key: 9472,
- meeting_key: 1234,
- },
- {
- driver_number: 1,
- position: 2,
- 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: 3.456,
- number_of_laps: 78,
- points: 18,
- session_key: 9472,
- meeting_key: 1234,
- },
- {
- driver_number: 44,
- position: 5,
- name_acronym: 'HAM',
- full_name: 'Lewis Hamilton',
- team_name: 'Ferrari',
- team_colour: 'e8002d',
- dnf: false,
- dns: false,
- dsq: false,
- duration: null,
- gap_to_leader: 21.234,
- number_of_laps: 78,
- points: 10,
- session_key: 9472,
- meeting_key: 1234,
- },
-]
-
-const mockGrid: 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: 1234,
- lap_duration: 74.892,
- },
- {
- driver_number: 16,
- position: 3,
- name_acronym: 'LEC',
- full_name: 'Charles Leclerc',
- team_name: 'Ferrari',
- team_colour: 'e8002d',
- session_key: 9472,
- meeting_key: 1234,
- lap_duration: 75.123,
- },
-]
-
-describe('ClassificationTable', () => {
- it('renders driver acronyms', () => {
- render( )
- expect(screen.getByText('LEC')).toBeInTheDocument()
- expect(screen.getByText('VER')).toBeInTheDocument()
- expect(screen.getByText('HAM')).toBeInTheDocument()
- })
-
- it('renders P1 badge for LEC', () => {
- const { container } = render( )
- const p1 = container.querySelector('.pos-p1')
- expect(p1).toBeInTheDocument()
- expect(p1?.textContent).toBe('1')
- })
-
- it('shows grid gain arrow for LEC (started P3, finished P1)', () => {
- render( )
- expect(screen.getByText('↑2')).toBeInTheDocument()
- })
-
- it('shows grid loss arrow for VER (started P1, finished P2)', () => {
- render( )
- expect(screen.getByText('↓1')).toBeInTheDocument()
- })
-
- it('renders missing notice when results are empty', () => {
- render( )
- expect(screen.getByText(/not ingested/i)).toBeInTheDocument()
- })
-})
-
-describe('ClassificationTable — DNF/DNS/DSQ', () => {
- it('shows DNF label', () => {
- const dnfResult: EnrichedResult = {
- ...mockResults[0],
- driver_number: 23,
- name_acronym: 'ALB',
- position: 20,
- dnf: true,
- duration: null,
- gap_to_leader: null,
- points: 0,
- }
- render( )
- expect(screen.getByText('DNF')).toBeInTheDocument()
- })
-})
diff --git a/frontend/src/test/PositionEvolutionView.test.tsx b/frontend/src/test/PositionEvolutionView.test.tsx
deleted file mode 100644
index 0fba545..0000000
--- a/frontend/src/test/PositionEvolutionView.test.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-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(
-
- )
- expect(container.querySelector('[data-testid="position-chart"]')).toBeInTheDocument()
- })
-
- it('renders an SVG chart', () => {
- const { container } = render(
-
- )
- expect(container.querySelector('svg')).toBeInTheDocument()
- expect(container.querySelectorAll('polyline').length).toBeGreaterThan(0)
- })
-
- it('does not show the missing-data notice', () => {
- render(
-
- )
- expect(screen.queryByText(/Lap-by-lap positions not available/i)).not.toBeInTheDocument()
- })
-
- it('renders Grid → Finish table below chart', () => {
- render(
-
- )
- expect(screen.getByText('Grid → Finish')).toBeInTheDocument()
- })
-})
-
-describe('PositionEvolutionView — positions missing', () => {
- it('shows the missing-data notice', () => {
- render(
-
- )
- 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(
-
- )
- 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(
-
- )
- expect(container.querySelector('[data-testid="position-chart"]')).not.toBeInTheDocument()
- })
-})
diff --git a/frontend/src/test/RaceHubPage.test.tsx b/frontend/src/test/RaceHubPage.test.tsx
index 33f26d3..4e9eb1c 100644
--- a/frontend/src/test/RaceHubPage.test.tsx
+++ b/frontend/src/test/RaceHubPage.test.tsx
@@ -234,10 +234,8 @@ describe('RaceHubPage', () => {
fireEvent.click(screen.getByRole('tab', { name: 'Race Story' }))
- expect(screen.getByRole('tab', { name: 'Classification' })).toBeInTheDocument()
- expect(screen.getByRole('tab', { name: 'Starting Grid' })).toBeInTheDocument()
- expect(screen.getByRole('tab', { name: 'Positions' })).toBeInTheDocument()
- expect(screen.getByText('Final Classification')).toBeInTheDocument()
+ expect(screen.getByText('VER')).toBeInTheDocument()
+
})
it('keeps Data Status accessible and free of inline CLI guidance', async () => {