/** * FE-DAWARICH-TRAILUI-010 to FE-DAWARICH-TRAILUI-015: the recorded-route * overlay on the Leaflet renderer. * * Everything this component decides is a decision about legibility on top of * somebody else's map, and every one of them is the kind that quietly rots: * * - **two lines per segment, casing first.** The white casing is what keeps * the route visible over a satellite tile. It only works while it is drawn * *under* the coloured line, so the order of the two `map()` blocks is * load-bearing and not cosmetic. * - **`interactive: false` on both.** A Leaflet path without it grows an * invisible hit area that lies over the places and the planned route and * eats the clicks meant for them. Losing this flag breaks selection on a * surface that has nothing to do with Dawarich, which is why it is pinned * here rather than left to a reviewer's eye. * - **colour through `pathOptions`**, because react-leaflet only calls * `setStyle` when that object's identity changes; a bare `color` prop sticks * at its mount-time value and a day recolour never arrives. * - **the day filter keeps the day's colour.** The colour index comes from the * day's place in the whole track, so scrubbing to a single day dims the rest * of the trip instead of repainting the day the user is looking at. * * The empty case matters too: returning `null` rather than an empty fragment is * what stops Leaflet from being handed zero-length polylines on every trip that * has no recording. */ import { render, screen } from '@testing-library/react'; import type { DawarichTrack, DawarichTrackDay } from '@trek/shared'; import { describe, expect, it, vi } from 'vitest'; import { DAWARICH_DAY_COLORS, DAWARICH_TRAIL_CASING } from './dawarichTrail'; // Leaflet itself is irrelevant here. What is under test is which polylines are // asked for and with what options, so the mock reflects the props back as data // attributes and the assertions read them in render order. vi.mock('react-leaflet', () => ({ Polyline: ({ positions, pathOptions, pane, interactive, }: { positions: Array<[number, number]>; pathOptions?: Record; pane?: string; interactive?: boolean; }) => (
), })); import DawarichTrailLayer from './DawarichTrailLayer'; function day(date: string, ...segments: Array>): DawarichTrackDay { return { date, segments: segments.map((points, i) => ({ points, mode: 'walking', startedAt: date + 'T08:0' + i + ':00.000Z', endedAt: date + 'T09:0' + i + ':00.000Z', distanceMeters: 1200, })), }; } function track(days: DawarichTrackDay[]): DawarichTrack { return { days, source: 'tracks', fetchedAt: '2026-05-03T10:00:00.000Z', pointCount: days.reduce((sum, d) => sum + d.segments.reduce((n, s) => n + s.points.length, 0), 0), truncated: false, }; } const DAY_ONE = day('2026-05-01', [ [48.2, 16.37], [48.21, 16.38], ]); const DAY_TWO = day( '2026-05-02', [ [45.46, 9.18], [45.47, 9.19], ], [ [45.5, 9.2], [45.51, 9.21], ] ); const lines = () => screen.queryAllByTestId('polyline'); describe('DawarichTrailLayer', () => { it('FE-DAWARICH-TRAILUI-010: draws nothing at all when there is no recording', () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); it('FE-DAWARICH-TRAILUI-011: ignores a segment that is a single fix rather than a line', () => { // Dawarich reports a lone point for a day someone only opened the app on. // Leaflet would accept the one-point polyline and draw nothing visible, but // it would still be a layer, and every such day would add two of them. const { container } = render(); expect(container.firstChild).toBeNull(); }); it('FE-DAWARICH-TRAILUI-012: draws every casing first, then every coloured line on top', () => { render(); const drawn = lines(); expect(drawn).toHaveLength(6); // three segments, casing + line each // The first half is the casing pass. Reversing these two blocks would put // the white line over the coloured one and the overlay would go white. const casings = drawn.slice(0, 3); const coloured = drawn.slice(3); expect(casings.every((el) => el.getAttribute('data-color') === DAWARICH_TRAIL_CASING)).toBe(true); expect(coloured.some((el) => el.getAttribute('data-color') === DAWARICH_TRAIL_CASING)).toBe(false); }); it('FE-DAWARICH-TRAILUI-013: gives the casing the pane and the weight that make it a casing, and nothing catches clicks', () => { render(); const [casing, line] = lines(); expect(casing.getAttribute('data-pane')).toBe('trek-track-casing'); expect(casing.getAttribute('data-weight')).toBe('6'); expect(casing.getAttribute('data-opacity')).toBe('0.55'); // Only the casing takes the pane; the coloured line is left in Leaflet's // default overlay pane. Pinned as the fact it is rather than as a rule: // that pane also holds the planned route, and the trail is mounted after // it, so the recorded line currently draws over the plan while the GL twin // puts both of its layers under it. expect(line.getAttribute('data-pane')).toBe(''); expect(line.getAttribute('data-weight')).toBe('3'); expect(line.getAttribute('data-dash')).toBe('6 5'); expect(line.getAttribute('data-points')).toBe( JSON.stringify([ [48.2, 16.37], [48.21, 16.38], ]) ); expect(lines().every((el) => el.getAttribute('data-interactive') === 'false')).toBe(true); }); it('FE-DAWARICH-TRAILUI-014: works without a casing pane on a renderer that has none', () => { render(); // An older Leaflet build, or the GL renderer's Leaflet fallback, has no // pane API and MapView passes undefined. Everything then lands in the // default overlay pane in insertion order, which still keeps the casing // under its own line, the one piece of the stacking that has to hold. expect(lines().every((el) => el.getAttribute('data-pane') === '')).toBe(true); expect(lines()).toHaveLength(2); }); it('FE-DAWARICH-TRAILUI-015: scrubbing to one day keeps that day its own colour', () => { const { rerender } = render(); // Two segments on the second day, so two casings and two lines, and the hue // is the SECOND of the palette even though it is now the only day drawn. expect(lines()).toHaveLength(4); expect( lines() .slice(2) .map((el) => el.getAttribute('data-color')) ).toEqual([DAWARICH_DAY_COLORS[1], DAWARICH_DAY_COLORS[1]]); rerender(); expect( lines() .slice(1) .map((el) => el.getAttribute('data-color')) ).toEqual([DAWARICH_DAY_COLORS[0]]); // A day nobody recorded on falls back to the empty case rather than to the // whole trip, which would be the worst possible answer to "show me Sunday". rerender(); expect(lines()).toHaveLength(0); }); });