import { describe, expect, it, vi } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import { TrackStatusBanner } from '../components/live/TrackStatusBanner' import { WeatherStrip } from '../components/live/WeatherStrip' import { StintHistory } from '../components/live/StintHistory' import { BattleChips } from '../components/live/BattleChips' import { GapSparkline } from '../components/live/GapSparkline' import { PinnedDrivers } from '../components/live/PinnedDrivers' import { TimingTower } from '../components/live/TimingTower' import { TrackMap } from '../components/live/TrackMap' import { detectBattles } from '../lib/battles' import type { LiveTimingRow } from '../lib/live' import type { LiveDriverData, LiveWeatherData, TrackOutline } from '../types' function makeRow( number: string, position: number, tla: string, driver: Partial = {}, ): LiveTimingRow { return { RacingNumber: number, Position: position, Driver: { RacingNumber: number, Position: position, Interval: '', GapToLeader: '', LastLapTime: '1:14.000', BestLapTime: '1:13.500', NumberOfLaps: 10, InPit: false, PitOut: false, Retired: false, ...driver, } as LiveDriverData, Info: { RacingNumber: number, BroadcastName: '', Tla: tla, TeamName: '', TeamColour: '3671c6', FirstName: '', LastName: '', }, } } describe('TrackStatusBanner', () => { it('renders a safety car banner for status 4', () => { render() const banner = screen.getByTestId('track-banner') expect(banner).toHaveClass('track-banner-sc') expect(banner).toHaveTextContent('SAFETY CAR') }) it('renders a neutral banner for unknown statuses', () => { render() const banner = screen.getByTestId('track-banner') expect(banner).toHaveClass('track-banner-unknown') expect(banner).toHaveTextContent('TRACK STATUS 42') }) it('renders nothing without a status', () => { render() expect(screen.queryByTestId('track-banner')).not.toBeInTheDocument() }) }) describe('WeatherStrip', () => { const weather: LiveWeatherData = { AirTemp: 21.4, TrackTemp: 34.8, Humidity: 58, WindSpeed: 2.3, WindDir: 180, Rainfall: true, } it('renders all populated weather fields', () => { render() const strip = screen.getByTestId('weather-strip') expect(strip).toHaveTextContent('21°C') expect(strip).toHaveTextContent('35°C') expect(strip).toHaveTextContent('58%') expect(strip).toHaveTextContent('2.3 m/s S') expect(strip).toHaveTextContent('RAIN') }) it('hides gracefully when the payload is empty', () => { render( , ) expect(screen.queryByTestId('weather-strip')).not.toBeInTheDocument() }) it('hides when weather is missing entirely', () => { render() expect(screen.queryByTestId('weather-strip')).not.toBeInTheDocument() }) }) describe('StintHistory', () => { it('renders the compound sequence with lap counts', () => { render( , ) const seq = screen.getByTestId('stint-seq') expect(seq).toHaveTextContent('M') expect(seq).toHaveTextContent('12') expect(seq).toHaveTextContent('H') expect(seq).toHaveTextContent('20') }) it('renders a dash without stint data', () => { render() expect(screen.getByText('-')).toBeInTheDocument() }) }) describe('GapSparkline', () => { it('shows a placeholder with too few samples', () => { render() expect(screen.queryByTestId('gap-spark')).not.toBeInTheDocument() }) it('renders a closing indicator when the gap shrinks', () => { render() expect(screen.getByTestId('gap-spark')).toHaveClass('trend-closing') expect(screen.getByTitle('Gap closing')).toBeInTheDocument() }) }) describe('BattleChips', () => { it('renders chip labels for detected battles', () => { const battles = detectBattles( [makeRow('1', 1, 'VER'), makeRow('4', 2, 'NOR', { Interval: '+0.4' })], 'Race', ) render() expect(screen.getByTestId('battle-chips')).toHaveTextContent('VER ⚔ NOR +0.4') }) it('renders nothing when there are no battles', () => { render() expect(screen.queryByTestId('battle-chips')).not.toBeInTheDocument() }) }) describe('TimingTower', () => { const rows = [ makeRow('1', 1, 'VER'), makeRow('4', 2, 'NOR', { Interval: '+0.4', GapToLeader: '+0.4' }), makeRow('16', 3, 'LEC', { Interval: '+3.2', GapToLeader: '+3.6' }), ] it('highlights battle rows and marks pinned drivers', () => { render( {}} />, ) expect(screen.getByText('VER').closest('tr')).toHaveClass('battle-row') expect(screen.getByText('NOR').closest('tr')).toHaveClass('battle-row') const lecRow = screen.getByText('LEC').closest('tr') expect(lecRow).not.toHaveClass('battle-row') expect(lecRow).toHaveClass('pinned-row') }) it('toggles a pin when the pin button is clicked', () => { const onTogglePin = vi.fn() render() fireEvent.click(screen.getByText('NOR').closest('tr')!.querySelector('.pin-btn')!) expect(onTogglePin).toHaveBeenCalledWith('4') }) it('shows an empty notice without rows', () => { render() expect(screen.getByText(/no driver timing rows/i)).toBeInTheDocument() }) it('annotates DRS-range intervals in race mode', () => { const raceRows = [ makeRow('1', 1, 'VER'), makeRow('4', 2, 'NOR', { Interval: '+0.4', GapToLeader: '+0.4' }), ] render( , ) expect(screen.getByText('DRS range')).toBeInTheDocument() }) it('renders the SQ1 cutoff after P17 and marks rows below as at risk', () => { const sprintRows = Array.from({ length: 22 }, (_, index) => makeRow(String(index + 1), index + 1, `D${index + 1}`), ) render( , ) expect(screen.getByTestId('qualifying-cutoff')).toHaveTextContent('SQ1 cutoff') expect(screen.getByTestId('qualifying-cutoff')).toHaveTextContent('P17 advance') expect(screen.getByText('D18').closest('tr')).toHaveClass('danger-row') expect(screen.getByText('D17').closest('tr')).not.toHaveClass('danger-row') }) }) describe('PinnedDrivers', () => { it('renders pinned cards with gap and unpins on click', () => { const onToggle = vi.fn() render( , ) const strip = screen.getByTestId('pinned-strip') expect(strip).toHaveTextContent('NOR') expect(strip).toHaveTextContent('+0.4') fireEvent.click(screen.getByText('NOR').closest('button')!) expect(onToggle).toHaveBeenCalledWith('4') }) it('renders a fallback card when the driver is missing from the feed', () => { render( {}} />, ) expect(screen.getByTestId('pinned-strip')).toHaveTextContent('no data') }) it('renders nothing without pins', () => { render( {}} />) expect(screen.queryByTestId('pinned-strip')).not.toBeInTheDocument() }) }) describe('TrackMap', () => { const outline: TrackOutline = { circuit_key: 9, bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 }, points: [ { x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }, ], } it('renders car dots and opens mini telemetry on tap', () => { render( , ) expect(screen.getByTestId('track-map')).toHaveTextContent('2 cars') fireEvent.click(screen.getByRole('button', { name: /NOR telemetry/i })) const readout = screen.getByTestId('track-telemetry') expect(readout).toHaveTextContent('NOR') expect(readout).toHaveTextContent('302km/h') expect(readout).toHaveTextContent('88%') expect(readout).toHaveTextContent('DRS') expect(readout).toHaveTextContent('10') expect(screen.getByRole('button', { name: /PIA telemetry/i })).toHaveClass('track-car-inactive') }) it('renders an empty state without outline data', () => { render() expect(screen.getByTestId('track-map')).toHaveTextContent(/track outline unavailable/i) }) })