// FE-COMP-MAPPLUGINLAYERS-001 to FE-COMP-MAPPLUGINLAYERS-009 import React from 'react' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { http, HttpResponse } from 'msw' import { render, screen, waitFor } from '../../../tests/helpers/render' import { server } from '../../../tests/helpers/msw/server' import type { PluginMapLayer, PluginMapLayerFeature } from '../../api/client' interface PathProps { positions?: [number, number][] center?: [number, number] radius?: number pathOptions?: Record children?: React.ReactNode } // react-leaflet primitives are replaced by markers that surface exactly what the // component controls: geometry, the path options and the pane it draws into. const mapHolder = vi.hoisted(() => ({ current: {} as Record })) vi.mock('react-leaflet', () => ({ Polyline: ({ positions, pathOptions, children }: PathProps) => (
{children}
), Polygon: ({ positions, pathOptions, children }: PathProps) => (
{children}
), Circle: ({ center, radius, pathOptions, children }: PathProps) => (
{children}
), Tooltip: ({ children }: { children?: React.ReactNode }) => {children}, useMap: () => mapHolder.current, })) import { PluginMapLayers } from './MapPluginLayers' function paneMap(preexisting = false) { const panes = new Map() if (preexisting) panes.set('trek-plugin-layers', document.createElement('div')) return { panes, getPane: vi.fn((name: string) => panes.get(name)), createPane: vi.fn((name: string) => { const el = document.createElement('div') panes.set(name, el) return el }), } } function feature(overrides: Partial = {}): PluginMapLayerFeature { return { type: 'polyline', points: [[48.85, 2.35], [48.86, 2.36]], tone: 'default', width: 3, dash: 'solid', opacity: 0.8, fill: false, ...overrides, } } function layer(features: PluginMapLayerFeature[], overrides: Partial = {}): PluginMapLayer { return { pluginId: 'ev-router', id: 'corridor', features, ...overrides } } function serveLayers(layers: PluginMapLayer[]) { server.use(http.get('/api/map-layers/:tripId', () => HttpResponse.json({ layers }))) } const optionsOf = (el: HTMLElement) => JSON.parse(el.getAttribute('data-path-options') || '{}') as Record beforeEach(() => { mapHolder.current = paneMap() as unknown as Record }) afterEach(() => { vi.clearAllMocks() }) describe('PluginMapLayers', () => { it('FE-COMP-MAPPLUGINLAYERS-001: renders nothing without a trip', async () => { let requests = 0 server.use(http.get('/api/map-layers/:tripId', () => { requests += 1; return HttpResponse.json({ layers: [] }) })) render() await waitFor(() => expect(screen.queryByTestId('polyline')).toBeNull()) expect(requests).toBe(0) }) it('FE-COMP-MAPPLUGINLAYERS-002: creates the below-overlay pane and draws into it', async () => { const map = paneMap() mapHolder.current = map as unknown as Record serveLayers([layer([feature()])]) render() const line = await screen.findByTestId('polyline') expect(map.createPane).toHaveBeenCalledWith('trek-plugin-layers') // Core geometry (overlayPane, 400) has to stay on top of plugin shapes. expect(map.panes.get('trek-plugin-layers')?.style.zIndex).toBe('399') expect(optionsOf(line).pane).toBe('trek-plugin-layers') }) it('FE-COMP-MAPPLUGINLAYERS-003: reuses an existing pane instead of creating a second one', async () => { const map = paneMap(true) mapHolder.current = map as unknown as Record serveLayers([layer([feature()])]) render() await screen.findByTestId('polyline') expect(map.createPane).not.toHaveBeenCalled() }) it('FE-COMP-MAPPLUGINLAYERS-004: still draws on a map without the pane API, just without a pane', async () => { mapHolder.current = {} serveLayers([layer([feature()])]) render() const line = await screen.findByTestId('polyline') expect(optionsOf(line)).not.toHaveProperty('pane') }) it('FE-COMP-MAPPLUGINLAYERS-005: draws polylines, polygons and metric circles', async () => { serveLayers([layer([ feature({ type: 'polyline' }), feature({ type: 'polygon', points: [[48, 2], [48, 3], [49, 3]] }), feature({ type: 'circle', points: undefined, center: [48.8, 2.3], radiusM: 750 }), ])]) render() await screen.findByTestId('polyline') expect(JSON.parse(screen.getByTestId('polygon').getAttribute('data-points') || '')).toEqual([[48, 2], [48, 3], [49, 3]]) const circle = screen.getByTestId('circle') expect(JSON.parse(circle.getAttribute('data-center') || '')).toEqual([48.8, 2.3]) expect(circle.getAttribute('data-radius')).toBe('750') }) it('FE-COMP-MAPPLUGINLAYERS-006: maps tone, width, dash and fill onto the leaflet path options', async () => { serveLayers([layer([ feature({ tone: 'danger', width: 6, dash: 'dash', opacity: 0.5 }), feature({ type: 'polygon', tone: 'success', dash: 'dot', opacity: 0.9, fill: true }), ])]) render() const line = await screen.findByTestId('polyline') expect(optionsOf(line)).toMatchObject({ color: '#ef4444', weight: 6, opacity: 0.5, dashArray: '8, 8', fill: false, fillOpacity: 0, }) // A filled shape is capped at 0.25 so it never hides the basemap underneath. expect(optionsOf(screen.getByTestId('polygon'))).toMatchObject({ color: '#10b981', dashArray: '1, 7', fill: true, fillOpacity: 0.25, }) }) it('FE-COMP-MAPPLUGINLAYERS-007: only a labelled feature gets a tooltip and stays clickable', async () => { serveLayers([layer([ feature({ label: 'Charging corridor' }), feature({ type: 'polygon' }), ])]) render() const line = await screen.findByTestId('polyline') expect(screen.getByTestId('tooltip')).toHaveTextContent('Charging corridor') expect(optionsOf(line).interactive).toBe(true) // Unlabelled shapes must never steal a map click. expect(optionsOf(screen.getByTestId('polygon')).interactive).toBe(false) expect(screen.getAllByTestId('tooltip')).toHaveLength(1) }) it('FE-COMP-MAPPLUGINLAYERS-008: skips features whose geometry is incomplete', async () => { serveLayers([layer([ feature({ type: 'polyline', points: undefined }), feature({ type: 'polygon', points: undefined }), feature({ type: 'circle', points: undefined, center: [48, 2] }), feature({ type: 'circle', points: undefined, radiusM: 500 }), feature({ label: 'kept' }), ])]) render() await screen.findByTestId('polyline') expect(screen.getAllByTestId('polyline')).toHaveLength(1) expect(screen.queryByTestId('polygon')).toBeNull() expect(screen.queryByTestId('circle')).toBeNull() }) it('FE-COMP-MAPPLUGINLAYERS-009: a failing request leaves the map without plugin overlays', async () => { serveLayers([layer([feature()])]) const { rerender } = render() await screen.findByTestId('polyline') server.use(http.get('/api/map-layers/:tripId', () => HttpResponse.json({ error: 'boom' }, { status: 500 }))) rerender() await waitFor(() => expect(screen.queryByTestId('polyline')).toBeNull()) }) })