import { describe, it, expect, vi } from 'vitest'; import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { generateRideGpx, generateRideCsv, synthesizeTelemetryTrack, analyzeCorners, } from '../lib/telemetry-export'; import { WebTelemetryReplayModal } from '../components/web-telemetry-replay-modal'; import { Ride } from '../components/app-cloud-data'; const mockRide: Ride = { id: 'ride_unit_test_123', startedAt: new Date('2026-09-28T14:30:00Z'), endedAt: new Date('2026-09-28T15:15:00Z'), distanceMiles: 24.8, topSpeedMph: 86.4, averageSpeed: 48.2, maxLeanLeft: 42.1, maxLeanRight: 39.5, maxAccelG: 0.72, maxBrakingG: 0.95, movingTime: 1850, elevationGain: 1240, qualityScore: 98, motorcycle: '2024 Yamaha YZF-R6', }; describe('Web Telemetry Replay & Export (WEB-23)', () => { it('synthesizes realistic 20 Hz telemetry points matching ride bounds', () => { const points = synthesizeTelemetryTrack(mockRide); expect(points.length).toBeGreaterThan(50); // Verify properties exist and have valid numerical ranges const first = points[0]; expect(first.timestamp).toBeDefined(); expect(first.latitude).toBeGreaterThan(30); expect(first.longitude).toBeLessThan(-80); expect(first.speed).toBeGreaterThanOrEqual(0); expect(first.leanAngle).toBeDefined(); expect(first.longitudinalG).toBeDefined(); expect(first.lateralG).toBeDefined(); }); it('generates valid GPX 1.1 with trackpoints, elevation, speed, and lean extensions', () => { const points = synthesizeTelemetryTrack(mockRide); const gpx = generateRideGpx(mockRide, points); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); }); it('generates compliant 20 Hz CSV matching ExportService format', () => { const points = synthesizeTelemetryTrack(mockRide); const csv = generateRideCsv(mockRide, points); const lines = csv.split('\n'); expect(lines[0]).toBe( 'timestamp,latitude,longitude,altitude,speed,heading,gpsAccuracy,leanAngle,pitchAngle,accelX,accelY,accelZ,longitudinalG,lateralG' ); expect(lines.length).toBe(points.length + 1); // Inspect first sample row const firstRowCols = lines[1].split(','); expect(firstRowCols.length).toBe(14); }); it('analyzes corners and grades apex performance correctly', () => { const points = synthesizeTelemetryTrack(mockRide); const corners = analyzeCorners(points); expect(Array.isArray(corners)).toBe(true); if (corners.length > 0) { const c = corners[0]; expect(c.direction).toMatch(/Left|Right/); expect(c.entrySpeed).toBeGreaterThan(0); expect(c.apexSpeed).toBeGreaterThan(0); expect(c.apexLeanAngle).toBeGreaterThan(15); expect(['A+', 'A', 'B', 'C', 'D']).toContain(c.grade); } }); it('renders WebTelemetryReplayModal with HUD gauges, transport controls, and export buttons', () => { const onClose = vi.fn(); render(); // Check title and metrics expect(screen.getByText('TELEMETRY FORENSIC REPLAY')).toBeDefined(); expect(screen.getByText(/2024 Yamaha YZF-R6/)).toBeDefined(); // Check HUD stats expect(screen.getByText('VEHICLE SPEED')).toBeDefined(); expect(screen.getByText('LEAN ANGLE')).toBeDefined(); expect(screen.getByText('G-FORCE DYNAMICS')).toBeDefined(); expect(screen.getByText('ALTITUDE & TIME')).toBeDefined(); // Check buttons expect(screen.getByTestId('export-gpx-btn')).toBeDefined(); expect(screen.getByTestId('export-csv-btn')).toBeDefined(); expect(screen.getByTestId('play-pause-btn')).toBeDefined(); expect(screen.getByTestId('replay-timeline-slider')).toBeDefined(); // Check tabs expect(screen.getByTestId('tab-map-replay')).toBeDefined(); expect(screen.getByTestId('tab-corner-grading')).toBeDefined(); // Switch to Corner Apex Grading tab fireEvent.click(screen.getByTestId('tab-corner-grading')); expect(screen.getByText('Corner')).toBeDefined(); expect(screen.getByText('Apex Lean')).toBeDefined(); expect(screen.getByText('Grade')).toBeDefined(); // Close modal fireEvent.click(screen.getByTestId('close-replay-modal')); expect(onClose).toHaveBeenCalled(); }); });