75 lines
6.2 KiB
TypeScript
75 lines
6.2 KiB
TypeScript
import React from 'react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
import { WebTelemetryReplayModal } from '../components/web-telemetry-replay-modal';
|
|
import type { Ride } from '../components/app-cloud-data';
|
|
const mocks=vi.hoisted(()=>({token:vi.fn().mockResolvedValue('authorized-test-token')}));
|
|
vi.mock('../lib/firebase-client',()=>({firebaseAuth:{currentUser:{uid:'sanitized-owner',getIdToken:mocks.token}}}));
|
|
const ride={id:'ride_sanitized',startedAt:new Date('2026-09-03T20:00:00Z'),movingTime:301,distanceMiles:15.66,topSpeedMph:83.7,averageSpeed:60,maxLeanLeft:27,maxLeanRight:26,telemetryBundlePath:'users/sanitized-owner/rides/ride_sanitized/telemetry.zip'} as Ride;
|
|
const recorded=[{timestamp:'2026-09-03T20:00:00Z',latitude:40,longitude:-74,speed:18,leanAngle:0}, {timestamp:'2026-09-03T20:00:01Z',latitude:40.001,longitude:-73.999,speed:20,leanAngle:25}];
|
|
beforeEach(()=>{ vi.spyOn(HTMLCanvasElement.prototype,'getContext').mockReturnValue(null); mocks.token.mockResolvedValue('authorized-test-token'); });
|
|
afterEach(()=>{cleanup();delete (window as Window & {L?:unknown}).L;vi.restoreAllMocks();vi.unstubAllGlobals();});
|
|
describe('recorded replay loading and identity',()=>{
|
|
it('keeps readable GPS metadata outside the interactive map viewport',async()=>{
|
|
const inline={...ride,telemetryBundlePath:undefined,points:recorded} as Ride;
|
|
render(<WebTelemetryReplayModal ride={inline} onClose={()=>{}}/>);
|
|
const source=screen.getByTestId('replay-data-source');
|
|
const badge=source.parentElement!;
|
|
const mapViewport=screen.getByTestId('telemetry-leaflet-map').parentElement!;
|
|
expect(badge.textContent).toContain('GPS: 40.00000, -74.00000');
|
|
expect(mapViewport.contains(badge)).toBe(false);
|
|
expect(badge.classList.contains('hidden')).toBe(false);
|
|
fireEvent.click(screen.getByTestId('tab-corner-grading'));
|
|
expect(badge.classList.contains('hidden')).toBe(true);
|
|
fireEvent.click(screen.getByTestId('tab-map-replay'));
|
|
expect(badge.classList.contains('hidden')).toBe(false);
|
|
});
|
|
it('shows loading, never a synthetic trace, and sends the existing Firebase identity',async()=>{
|
|
vi.stubGlobal('fetch',vi.fn(()=>new Promise(()=>{})));
|
|
render(<WebTelemetryReplayModal ride={ride} onClose={()=>{}}/>);
|
|
expect(screen.getByText('Loading recorded telemetry…')).toBeDefined();
|
|
expect(document.querySelector('canvas')).toBeNull();
|
|
await waitFor(()=>expect(fetch).toHaveBeenCalledWith(expect.stringContaining(ride.id),expect.objectContaining({headers:{Authorization:'Bearer authorized-test-token'},cache:'no-store'})));
|
|
});
|
|
it('shows retrieval errors instead of plausible invented GPS or an export',async()=>{
|
|
vi.stubGlobal('fetch',vi.fn().mockResolvedValue(new Response(JSON.stringify({success:false,error:'Recorded telemetry is unavailable.'}),{status:404})));
|
|
render(<WebTelemetryReplayModal ride={ride} onClose={()=>{}}/>);
|
|
await waitFor(()=>expect(screen.getByText('Recorded telemetry is unavailable.')).toBeDefined());
|
|
expect((screen.getByTestId('export-gpx-btn') as HTMLButtonElement).disabled).toBe(true);
|
|
expect(screen.queryByText(/35\.18520/)).toBeNull();
|
|
});
|
|
it('clears prior ride geometry during switching and ignores late responses from a closed ride',async()=>{
|
|
let resolveFirst!:(r:Response)=>void;
|
|
vi.stubGlobal('fetch',vi.fn().mockImplementationOnce(()=>new Promise(r=>{resolveFirst=r;})).mockResolvedValue(new Response(JSON.stringify({success:true,rideId:'ride_next',source:'r2-csv',points:recorded}))));
|
|
const {rerender}=render(<WebTelemetryReplayModal ride={ride} onClose={()=>{}}/>);
|
|
await waitFor(()=>expect(fetch).toHaveBeenCalledTimes(1));
|
|
rerender(<WebTelemetryReplayModal ride={{...ride,id:'ride_next'}} onClose={()=>{}}/>);
|
|
await waitFor(()=>expect(screen.getByText(/GPS: 40\.00000, -74\.00000/)).toBeDefined());
|
|
resolveFirst(new Response(JSON.stringify({success:true,rideId:ride.id,source:'r2-gpx',points:[{...recorded[0],latitude:12,longitude:13}]})));
|
|
await waitFor(()=>expect(screen.queryByText(/GPS: 12\.00000/)).toBeNull());
|
|
});
|
|
it('uses geographic Leaflet layers/bounds and recorded rider positions; cleans up on close and reopen',async()=>{
|
|
const map={invalidateSize:vi.fn(),remove:vi.fn(),fitBounds:vi.fn(),removeLayer:vi.fn(),panTo:vi.fn(),getBounds:()=>({contains:()=>true})};
|
|
const rider={addTo:vi.fn().mockReturnThis(),setLatLng:vi.fn(),setIcon:vi.fn(),bindTooltip:vi.fn().mockReturnThis(),on:vi.fn().mockReturnThis()};
|
|
const layer=()=>({addTo:vi.fn().mockReturnThis(),on:vi.fn().mockReturnThis(),addLayer:vi.fn()});
|
|
const L={map:vi.fn(()=>map),tileLayer:vi.fn(layer),featureGroup:vi.fn(layer),polyline:vi.fn((_coords:[number,number][],_options:unknown)=>layer()),marker:vi.fn(()=>rider),divIcon:vi.fn(),circleMarker:vi.fn(()=>rider),latLngBounds:vi.fn((coords)=>coords)};
|
|
(window as Window & {L?:unknown}).L=L;
|
|
const samples=[...recorded,{...recorded[1],timestamp:'2026-09-03T20:05:00Z',latitude:41,longitude:-73,segmentId:1},{...recorded[1],timestamp:'2026-09-03T20:05:01Z',latitude:41.001,longitude:-72.999,segmentId:1}];
|
|
const inline={...ride,telemetryBundlePath:undefined,points:samples} as Ride;
|
|
const {unmount}=render(<WebTelemetryReplayModal ride={inline} onClose={()=>{}}/>);
|
|
await waitFor(()=>expect(L.polyline).toHaveBeenCalledTimes(2));
|
|
expect(L.polyline.mock.calls.map(call=>call[0])).toEqual([[[40,-74],[40.001,-73.999]],[[41,-73],[41.001,-72.999]]]);
|
|
expect(map.fitBounds).toHaveBeenCalledWith([[40,-74],[40.001,-73.999],[41,-73],[41.001,-72.999]],{padding:[35,35]});
|
|
fireEvent.change(screen.getByTestId('replay-timeline-slider'),{target:{value:'3'}});
|
|
expect(rider.setLatLng).toHaveBeenLastCalledWith([41.001,-72.999]);
|
|
const resizeCalls=map.invalidateSize.mock.calls.length;
|
|
fireEvent(window,new Event('resize'));
|
|
expect(map.invalidateSize.mock.calls.length).toBeGreaterThan(resizeCalls);
|
|
unmount();
|
|
expect(map.remove).toHaveBeenCalledTimes(1);
|
|
render(<WebTelemetryReplayModal ride={inline} onClose={()=>{}}/>);
|
|
await waitFor(()=>expect(L.map).toHaveBeenCalledTimes(2));
|
|
expect(rider.setLatLng).toHaveBeenLastCalledWith([40,-74]);
|
|
});
|
|
});
|