import { describe, expect, it } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import { TelemetryTraceChart, type TelemetryTraceSeries, } from '../components/charts/TelemetryTraceChart' function makeSeries(label: string, color: string, speeds: number[]): TelemetryTraceSeries { return { label, color, samples: speeds.map((speed, i) => ({ speed, throttle: (i * 25) % 101, brake: i % 2 === 0 ? 100 : 0, })), } } const VER = makeSeries('VER', '#2563eb', [280, 310, 190, 90, 240]) const LEC = makeSeries('LEC', '#dc2626', [275, 305, 200, 95, 235]) describe('TelemetryTraceChart', () => { it('renders one polyline per series per channel', () => { const { container } = render() const lines = container.querySelectorAll('polyline.telemetry-trace-line') expect(lines).toHaveLength(2 * 3) for (const channel of ['speed', 'throttle', 'brake']) { expect( container.querySelectorAll(`polyline.telemetry-trace-line[data-channel="${channel}"]`), ).toHaveLength(2) } expect( container.querySelectorAll('polyline.telemetry-trace-line[data-series="VER"]'), ).toHaveLength(3) }) it('respects the channels prop', () => { const { container } = render() expect(container.querySelectorAll('polyline.telemetry-trace-line')).toHaveLength(1) expect(container.querySelectorAll('.telemetry-trace-panel')).toHaveLength(1) }) it('renders an empty state for no series', () => { render() expect(screen.getByText('No telemetry data')).toBeInTheDocument() }) it('renders an empty state when a series has no samples', () => { render() expect(screen.getByText('No telemetry data')).toBeInTheDocument() }) it('survives a single sample and mismatched series lengths', () => { const short = makeSeries('VER', '#2563eb', [300]) const long = makeSeries('LEC', '#dc2626', [280, 290, 300]) const { container } = render() // Clamped to the shortest series: every polyline has exactly one point. const lines = container.querySelectorAll('polyline.telemetry-trace-line') expect(lines).toHaveLength(6) for (const line of lines) { expect(line.getAttribute('points')!.trim().split(' ')).toHaveLength(1) } }) it('clamps throttle values to 0-100 within the panel', () => { const wild: TelemetryTraceSeries = { label: 'VER', color: '#2563eb', samples: [ { speed: 100, throttle: 150, brake: 0 }, { speed: 100, throttle: -20, brake: 0 }, { speed: 100, throttle: 50, brake: 0 }, ], } const { container } = render( , ) const line = container.querySelector('polyline.telemetry-trace-line[data-channel="throttle"]')! const ys = line .getAttribute('points')! .split(' ') .map((p) => Number(p.split(',')[1])) // Panel occupies y 0..110 with 6px padding; clamped values pin to the edges. expect(ys[0]).toBeCloseTo(6, 1) // 150% -> 100% -> panel top expect(ys[1]).toBeCloseTo(104, 1) // -20% -> 0% -> panel bottom expect(ys[2]).toBeCloseTo(55, 1) // 50% -> middle }) it('shows the speed axis max in km/h', () => { render() expect(screen.getByText('310 km/h')).toBeInTheDocument() }) it('lists every series in the legend with a color swatch', () => { const { container } = render() const legend = container.querySelector('.telemetry-trace-legend')! expect(legend.textContent).toContain('VER') expect(legend.textContent).toContain('LEC') expect(legend.querySelectorAll('.telemetry-trace-swatch')).toHaveLength(2) }) it('shows a crosshair and per-driver readout on hover, hides on leave', () => { const { container } = render() const svg = container.querySelector('svg.telemetry-trace-svg')! expect(screen.queryByTestId('telemetry-trace-crosshair')).not.toBeInTheDocument() fireEvent.mouseMove(svg, { clientX: 0, clientY: 10 }) expect(screen.getByTestId('telemetry-trace-crosshair')).toBeInTheDocument() const readout = screen.getByTestId('telemetry-trace-readout') expect(readout.textContent).toContain('VER') expect(readout.textContent).toContain('LEC') expect(readout.textContent).toContain('km/h') fireEvent.mouseLeave(svg) expect(screen.queryByTestId('telemetry-trace-crosshair')).not.toBeInTheDocument() }) })