diff --git a/frontend/src/components/RaceStoryCanvas.tsx b/frontend/src/components/RaceStoryCanvas.tsx index 1318acf..9d5f62f 100644 --- a/frontend/src/components/RaceStoryCanvas.tsx +++ b/frontend/src/components/RaceStoryCanvas.tsx @@ -417,7 +417,10 @@ export function RaceStoryCanvas({ data }: Props) { clearChapterSelection() if (!svgRef.current) return const rect = svgRef.current.getBoundingClientRect() - const x = e.clientX - rect.left + if (rect.width <= 0) return + // Pointer coordinates are CSS pixels; convert them to the SVG viewBox + // before comparing with the fixed chart margins and plot width. + const x = ((e.clientX - rect.left) / rect.width) * W const t = Math.max(0, Math.min(1, (x - PL) / plotW)) setScrubTime(t) } @@ -595,6 +598,7 @@ export function RaceStoryCanvas({ data }: Props) { width={plotW} height={plotH} fill="transparent" + data-testid="position-chart-interaction" onPointerMove={handlePointerMove} onPointerLeave={() => { if (!isPlaying) { diff --git a/frontend/src/test/RaceStoryCanvas.test.tsx b/frontend/src/test/RaceStoryCanvas.test.tsx index c45c859..b822f25 100644 --- a/frontend/src/test/RaceStoryCanvas.test.tsx +++ b/frontend/src/test/RaceStoryCanvas.test.tsx @@ -206,6 +206,30 @@ describe('RaceStoryCanvas replay map', () => { expect(document.querySelector('.rs-driver-line')).toHaveAttribute('points', '40,8 592,8 592,8') }) + it('maps cursor positions from rendered pixels to the SVG viewBox', () => { + renderCanvas() + + const svg = document.querySelector('.rs-position-chart-svg')! + vi.spyOn(svg, 'getBoundingClientRect').mockReturnValue({ + x: 0, + y: 0, + width: 1280, + height: 360, + top: 0, + right: 1280, + bottom: 360, + left: 0, + toJSON: () => ({}), + }) + + const pointerMove = new Event('pointermove', { bubbles: true }) + Object.defineProperty(pointerMove, 'clientX', { value: 640 }) + fireEvent(screen.getByTestId('position-chart-interaction'), pointerMove) + + // 640px is the middle of a 1280px rendered chart, which is x=320 in its 640-unit viewBox. + expect(document.querySelector('.rs-playhead')).toHaveAttribute('x1', '320') + }) + it('syncs active chapter highlight when a chapter card is clicked', async () => { renderCanvas()