Files
TREK/client/src/components/Map/viaMarkerState.test.ts
T
Maurice c1f7587704 fix(roadtrip): a freshly placed via lost its first drag
Placing one triggers a re-route, the re-route lands about a second later, and
the render it causes rebuilt every via handle — which is exactly when somebody
reaches for the dot they just made. On the GL renderer that rebuild replaces
the DOM element outright, and a pointerdown belongs to the element it happened
on, so the drag went to an element that no longer existed. The second attempt
worked because by then nothing was pending, and an older via never showed it
because nothing was re-routing.

Two causes, both identity: the vias arrive as a fresh object on every reload
even when the rows came back identical, and `onMoveVia` is a fresh closure on
every render of the planner. Both were in the effect's dependencies.

The list is now keyed by what a handle is built from — id and position — so it
only changes when a via does, and the callbacks are read through a ref. A via
that genuinely moved still gets a new list, which is what redraws it where it
now is; the test pins both halves, because holding the element still must not
mean holding it in the wrong place.

Leaflet's markers had the same churn without the same symptom: a fresh
`position` array called `setLatLng` on every marker on every render, and a
fresh `eventHandlers` object unbound and rebound every listener. Same fix.
2026-09-07 18:36:54 +02:00

69 lines
2.6 KiB
TypeScript

import { renderHook } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { useStableVias } from './viaMarkerState'
import type { RoadtripVia } from '@trek/shared'
/**
* The list both map renderers rebuild their via handles from.
*
* Its identity is the whole contract: the GL renderer replaces the DOM element when this
* changes, and a pointerdown belongs to the element it happened on. A list that changed
* on every render meant a freshly placed via lost its first drag to the re-route landing
* underneath it.
*/
const via = (over: Partial<RoadtripVia> & { id: number }): RoadtripVia => ({
day_id: 1, after_order_index: 0, sequence: 0, lat: 48.1, lng: 2.1, ...over,
})
describe('useStableVias', () => {
it('FE-VIASTATE-001: the same vias in a fresh object are the same list', () => {
// Exactly what a reload hands down: identical rows, a new object every time.
const { result, rerender } = renderHook(
({ byDay }) => useStableVias(byDay),
{ initialProps: { byDay: { 1: [via({ id: 5 })] } } },
)
const first = result.current
rerender({ byDay: { 1: [via({ id: 5 })] } })
expect(result.current).toBe(first)
})
it('FE-VIASTATE-002: a via that moved is a new list, so the handle is redrawn where it now is', () => {
const { result, rerender } = renderHook(
({ byDay }) => useStableVias(byDay),
{ initialProps: { byDay: { 1: [via({ id: 5 })] } } },
)
const first = result.current
rerender({ byDay: { 1: [via({ id: 5, lat: 48.9 })] } })
expect(result.current).not.toBe(first)
expect(result.current[0].lat).toBe(48.9)
})
it('FE-VIASTATE-003: adding or removing one is a new list too', () => {
const { result, rerender } = renderHook(
({ byDay }) => useStableVias(byDay),
{ initialProps: { byDay: { 1: [via({ id: 5 })] } } },
)
const first = result.current
rerender({ byDay: { 1: [via({ id: 5 }), via({ id: 6, lat: 48.4 })] } })
expect(result.current).not.toBe(first)
expect(result.current).toHaveLength(2)
})
it('FE-VIASTATE-004: every day of the trip is in one list, and no vias is a stable empty one', () => {
const { result, rerender } = renderHook(
({ byDay }) => useStableVias(byDay),
{ initialProps: { byDay: { 1: [via({ id: 5 })], 2: [via({ id: 6, day_id: 2 })] } as Record<number, RoadtripVia[]> | undefined },
})
expect(result.current.map(v => v.id)).toEqual([5, 6])
rerender({ byDay: undefined })
const empty = result.current
expect(empty).toEqual([])
rerender({ byDay: {} })
expect(result.current).toBe(empty)
})
})