Spaces:
Sleeping
Sleeping
| // FE-COMP-JOURNEYMAP-001 to FE-COMP-JOURNEYMAP-006 | |
| vi.mock('../../api/websocket', () => ({ | |
| connect: vi.fn(), | |
| disconnect: vi.fn(), | |
| getSocketId: vi.fn(() => null), | |
| setRefetchCallback: vi.fn(), | |
| setPreReconnectHook: vi.fn(), | |
| addListener: vi.fn(), | |
| removeListener: vi.fn(), | |
| })); | |
| // Leaflet does not work in jsdom — mock the entire library | |
| vi.mock('leaflet', () => { | |
| const mockMarker = { | |
| addTo: vi.fn().mockReturnThis(), | |
| bindTooltip: vi.fn().mockReturnThis(), | |
| on: vi.fn().mockReturnThis(), | |
| setIcon: vi.fn(), | |
| setZIndexOffset: vi.fn(), | |
| getLatLng: vi.fn(() => ({ lat: 0, lng: 0 })), | |
| }; | |
| const mockMap = { | |
| remove: vi.fn(), | |
| invalidateSize: vi.fn(), | |
| fitBounds: vi.fn(), | |
| setView: vi.fn(), | |
| flyTo: vi.fn(), | |
| getZoom: vi.fn(() => 10), | |
| zoomIn: vi.fn(), | |
| zoomOut: vi.fn(), | |
| }; | |
| return { | |
| default: { | |
| map: vi.fn(() => mockMap), | |
| tileLayer: vi.fn(() => ({ addTo: vi.fn() })), | |
| marker: vi.fn(() => mockMarker), | |
| polyline: vi.fn(() => ({ addTo: vi.fn() })), | |
| divIcon: vi.fn(() => ({})), | |
| latLngBounds: vi.fn(() => ({})), | |
| }, | |
| map: vi.fn(() => mockMap), | |
| tileLayer: vi.fn(() => ({ addTo: vi.fn() })), | |
| marker: vi.fn(() => mockMarker), | |
| polyline: vi.fn(() => ({ addTo: vi.fn() })), | |
| divIcon: vi.fn(() => ({})), | |
| latLngBounds: vi.fn(() => ({})), | |
| }; | |
| }); | |
| import React from 'react'; | |
| import { render } from '../../../tests/helpers/render'; | |
| import { resetAllStores, seedStore } from '../../../tests/helpers/store'; | |
| import { useSettingsStore } from '../../store/settingsStore'; | |
| import { buildSettings } from '../../../tests/helpers/factories'; | |
| import L from 'leaflet'; | |
| import JourneyMap from './JourneyMap'; | |
| import type { JourneyMapHandle } from './JourneyMap'; | |
| const entriesWithCoords = [ | |
| { id: 'e1', lat: 48.8566, lng: 2.3522, title: 'Paris', mood: null, entry_date: '2025-06-01' }, | |
| { id: 'e2', lat: 52.52, lng: 13.405, title: 'Berlin', mood: null, entry_date: '2025-06-02' }, | |
| ]; | |
| const entriesWithoutCoords = [ | |
| { id: 'e3', lat: 0, lng: 0, title: 'Unknown Place', mood: null, entry_date: '2025-06-03' }, | |
| ]; | |
| const mixedEntries = [ | |
| ...entriesWithCoords, | |
| ...entriesWithoutCoords, | |
| ]; | |
| beforeEach(() => { | |
| resetAllStores(); | |
| seedStore(useSettingsStore, { settings: buildSettings() }); | |
| vi.clearAllMocks(); | |
| }); | |
| describe('JourneyMap', () => { | |
| it('FE-COMP-JOURNEYMAP-001: renders map container', () => { | |
| const { container } = render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| // The component renders a div with a child div ref for the Leaflet map | |
| expect(container.firstChild).toBeInTheDocument(); | |
| expect(L.map).toHaveBeenCalled(); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-002: renders markers for entries with coordinates', () => { | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| // Two entries with valid lat/lng should produce two markers | |
| expect(L.marker).toHaveBeenCalledTimes(2); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-003: does not render markers for entries without coordinates', () => { | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithoutCoords} /> | |
| ); | |
| // Entry with lat=0 and lng=0 is filtered out by buildMarkerItems (if (e.lat && e.lng)) | |
| expect(L.marker).not.toHaveBeenCalled(); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-004: renders polyline connecting entries', () => { | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| // With 2+ marker items, a route polyline is drawn | |
| expect(L.polyline).toHaveBeenCalled(); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-005: shows entry title in marker tooltip', () => { | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| // Each marker calls bindTooltip with the entry label | |
| const mockMarkerInstance = (L.marker as any).mock.results[0].value; | |
| expect(mockMarkerInstance.bindTooltip).toHaveBeenCalledWith( | |
| 'Paris', | |
| expect.objectContaining({ direction: 'top' }), | |
| ); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-006: exposes imperative handle (focusMarker)', () => { | |
| const ref = React.createRef<JourneyMapHandle>(); | |
| render( | |
| <JourneyMap ref={ref} checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| expect(ref.current).not.toBeNull(); | |
| expect(typeof ref.current!.focusMarker).toBe('function'); | |
| expect(typeof ref.current!.highlightMarker).toBe('function'); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-007: renders SVG pin markers via divIcon', () => { | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| // Each marker is created with L.divIcon containing SVG html | |
| expect(L.divIcon).toHaveBeenCalledTimes(2); | |
| const firstCall = (L.divIcon as any).mock.calls[0][0]; | |
| expect(firstCall.html).toContain('<svg'); | |
| expect(firstCall.html).toContain('</svg>'); | |
| // Marker index label "1" for first entry | |
| expect(firstCall.html).toContain('>1<'); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-008: renders markers with mood-based entry labels', () => { | |
| const entriesWithMood = [ | |
| { id: 'e1', lat: 48.8566, lng: 2.3522, title: 'Happy Paris', mood: 'happy', entry_date: '2025-06-01' }, | |
| { id: 'e2', lat: 52.52, lng: 13.405, title: 'Sad Berlin', mood: 'sad', entry_date: '2025-06-02' }, | |
| ]; | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithMood} /> | |
| ); | |
| // Markers are still created (mood does not prevent rendering) | |
| expect(L.marker).toHaveBeenCalledTimes(2); | |
| // Tooltips use the entry titles | |
| const mockMarker1 = (L.marker as any).mock.results[0].value; | |
| expect(mockMarker1.bindTooltip).toHaveBeenCalledWith( | |
| 'Happy Paris', | |
| expect.objectContaining({ direction: 'top' }), | |
| ); | |
| const mockMarker2 = (L.marker as any).mock.results[1].value; | |
| expect(mockMarker2.bindTooltip).toHaveBeenCalledWith( | |
| 'Sad Berlin', | |
| expect.objectContaining({ direction: 'top' }), | |
| ); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-009: draws route polyline connecting multiple markers', () => { | |
| const threeEntries = [ | |
| { id: 'e1', lat: 48.8566, lng: 2.3522, title: 'Paris', mood: null, entry_date: '2025-06-01' }, | |
| { id: 'e2', lat: 52.52, lng: 13.405, title: 'Berlin', mood: null, entry_date: '2025-06-02' }, | |
| { id: 'e3', lat: 41.9028, lng: 12.4964, title: 'Rome', mood: null, entry_date: '2025-06-03' }, | |
| ]; | |
| render( | |
| <JourneyMap checkins={[]} entries={threeEntries} /> | |
| ); | |
| // Route polyline is drawn for items.length > 1 | |
| expect(L.polyline).toHaveBeenCalled(); | |
| const polylineCall = (L.polyline as any).mock.calls[0]; | |
| // Should contain coordinates for all three entries | |
| expect(polylineCall[0].length).toBe(3); | |
| // Verify dashed style | |
| expect(polylineCall[1]).toMatchObject({ dashArray: '4 6' }); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-010: fitBounds is called for auto-zoom', () => { | |
| // Trigger requestAnimationFrame synchronously | |
| const origRAF = globalThis.requestAnimationFrame; | |
| globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => { cb(0); return 0; }; | |
| render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| const mockMap = (L.map as any).mock.results[0].value; | |
| // fitBounds is called inside requestAnimationFrame with the collected coordinates | |
| expect(mockMap.fitBounds).toHaveBeenCalled(); | |
| expect(L.latLngBounds).toHaveBeenCalled(); | |
| globalThis.requestAnimationFrame = origRAF; | |
| }); | |
| it('FE-COMP-JOURNEYMAP-011: single entry creates marker but no polyline', () => { | |
| const singleEntry = [ | |
| { id: 'e1', lat: 48.8566, lng: 2.3522, title: 'Solo Paris', mood: null, entry_date: '2025-06-01' }, | |
| ]; | |
| render( | |
| <JourneyMap checkins={[]} entries={singleEntry} /> | |
| ); | |
| // One marker created | |
| expect(L.marker).toHaveBeenCalledTimes(1); | |
| // No route polyline — polyline is only drawn when items.length > 1 | |
| expect(L.polyline).not.toHaveBeenCalled(); | |
| }); | |
| it('FE-COMP-JOURNEYMAP-012: renders zoom control buttons', () => { | |
| const { container } = render( | |
| <JourneyMap checkins={[]} entries={entriesWithCoords} /> | |
| ); | |
| // The component renders zoom in (+) and zoom out (−) buttons | |
| const buttons = container.querySelectorAll('button'); | |
| expect(buttons.length).toBe(2); | |
| expect(buttons[0].textContent).toBe('+'); | |
| expect(buttons[1].textContent).toBe('−'); | |
| }); | |
| }); | |