126 lines
4.6 KiB
TypeScript
126 lines
4.6 KiB
TypeScript
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('<?xml version="1.0" encoding="UTF-8"?>');
|
|
expect(gpx).toContain('<gpx version="1.1" creator="MotorSignal"');
|
|
expect(gpx).toContain('<trk>');
|
|
expect(gpx).toContain('<trkseg>');
|
|
expect(gpx).toContain('<trkpt lat=');
|
|
expect(gpx).toContain('<ele>');
|
|
expect(gpx).toContain('<extensions>');
|
|
expect(gpx).toContain('<speed>');
|
|
expect(gpx).toContain('<lean>');
|
|
});
|
|
|
|
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(<WebTelemetryReplayModal ride={mockRide} onClose={onClose} />);
|
|
|
|
// 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();
|
|
});
|
|
});
|