/** * FE-MAP-ROUTE-001 to FE-MAP-ROUTE-004 * * The chat's routes on the Leaflet map: a rim under each line and nothing * but an invisible fat line taking the pointer; dashes for a way not chosen, * faint for the way a change replaces; the hard grades over the line, a disc * at the start and a ring at the end; the named stops only while lit; and * the pointer onto, along and off a line reported by its key; a line that * lights up brought to the front. What Leaflet makes of the options is * Leaflet's: the paths come back as data here. */ import { fireEvent, render, screen } from '@testing-library/react'; import type { ReactNode, Ref } from 'react'; import { describe, expect, it, vi } from 'vitest'; import type { AssistantRouteLine } from '../../types/assistantMap'; import { AssistantGhostLineLayer } from './AssistantGhostLineLayer'; type Handlers = Record void) | undefined>; const bringToFront = vi.hoisted(() => vi.fn()); vi.mock('react-leaflet', async () => { const { useImperativeHandle } = await vi.importActual('react'); return { FeatureGroup: ({ children, ref }: { children?: ReactNode; ref?: Ref<{ bringToFront: () => void }> }) => { useImperativeHandle(ref, () => ({ bringToFront })); return
{children}
; }, Polyline: ({ positions, pathOptions, interactive, eventHandlers, }: { positions: unknown; pathOptions?: Record; interactive?: boolean; eventHandlers?: Handlers; }) => (
eventHandlers?.mouseover?.()} onMouseLeave={() => eventHandlers?.mouseout?.()} onMouseMove={() => eventHandlers?.mousemove?.({ latlng: { lat: 47.78, lng: 12.6 } })} onClick={() => eventHandlers?.click?.({ latlng: { lat: 47.79, lng: 12.61 } })} /> ), CircleMarker: ({ center, pathOptions, children, }: { center: [number, number]; pathOptions?: Record; children?: ReactNode; }) => (
{children}
), Tooltip: ({ children }: { children?: ReactNode }) => {children}, }; }); const LINE: AssistantRouteLine = { id: 'c1:look:route', ref: 'c1', lines: [ [ [47.77, 12.59], [47.78, 12.6], [47.79, 12.62], ], ], color: '#0a84ff', casing: '#ffffff', ends: { start: [47.77, 12.59], end: [47.79, 12.62] }, stops: [{ lat: 47.78, lng: 12.6, label: 'Strausshütte' }], tints: [ { line: [ [47.78, 12.6], [47.79, 12.62], ], color: '#7c3aed', }, ], }; const paths = () => screen.queryAllByTestId('polyline'); /** What a path was asked for, as plain data. */ const asked = (el: HTMLElement | undefined) => ({ ...el?.dataset }); describe('AssistantGhostLineLayer', () => { it('FE-MAP-ROUTE-001: a rim, the line, its hard grade and its ends; only the invisible line takes the pointer', () => { render(); const [rim, core, grade, hit] = paths(); expect(asked(rim)).toMatchObject({ color: '#ffffff', weight: '7', interactive: 'false' }); expect(asked(core)).toMatchObject({ color: '#0a84ff', weight: '4', dash: '', interactive: 'false' }); expect(asked(grade)).toMatchObject({ color: '#7c3aed', interactive: 'false' }); expect(asked(hit)).toMatchObject({ weight: '16', opacity: '0', interactive: 'undefined' }); const discs = screen.getAllByTestId('disc'); expect(discs.map((disc) => disc.dataset.fill)).toEqual(['#0a84ff', '#ffffff']); // The stops show only while the line is lit. expect(screen.queryByTestId('stop-name')).not.toBeInTheDocument(); }); it('FE-MAP-ROUTE-002: dashed, faint, a loop with one disc, and the lit line with its named stops', () => { const loop: AssistantRouteLine = { ...LINE, dashed: true, dim: true, ends: { start: [47.77, 12.59], end: null } }; const { rerender } = render(); expect(asked(paths()[1])).toMatchObject({ dash: '6 7', opacity: '0.45' }); expect(screen.getAllByTestId('disc')).toHaveLength(1); rerender(); expect(screen.getByTestId('stop-name')).toHaveTextContent('Strausshütte'); rerender(); expect(paths()).toHaveLength(0); }); it('FE-MAP-ROUTE-003: the pointer onto, along and off a line, and a tap on it, report its key and where', () => { const onHover = vi.fn(); const onPoint = vi.fn(); render(); const hit = paths()[3] as HTMLElement; fireEvent.mouseEnter(hit); fireEvent.mouseMove(hit); fireEvent.click(hit); fireEvent.mouseLeave(hit); expect(onHover.mock.calls).toEqual([['c1'], [null]]); expect(onPoint.mock.calls).toEqual([ ['c1', 47.78, 12.6], ['c1', 47.79, 12.61], ]); }); it('FE-MAP-ROUTE-004: a dashed way that lights up loses its dashes and comes to the front; a calm one stays put', () => { bringToFront.mockClear(); const way: AssistantRouteLine = { ...LINE, dashed: true }; const other: AssistantRouteLine = { ...LINE, id: 'c2:look:route', ref: 'c2' }; const { rerender } = render(); expect(asked(paths()[1])).toMatchObject({ dash: '6 7' }); expect(bringToFront).not.toHaveBeenCalled(); // The same line, now lit, which the route model draws solid: its dash is asked away by name, // so Leaflet's merged style drops it. rerender(); const lit = screen.getAllByTestId('group')[1]; const core = lit?.querySelectorAll('[data-testid="polyline"]')[1]; expect(asked(core)).toMatchObject({ dash: '', dashNamed: 'true' }); expect(bringToFront).toHaveBeenCalledTimes(1); }); });