alessandro trinca tornidor
test(frontend): add sendMLRequest unit tests and PagePredictionMap conditional rendering tests
4a382c1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' | |
| /** | |
| * Tests for sendMLRequest (helpers.ts lines 43β73). | |
| * | |
| * sendMLRequest calls getGeoJSONRequest which calls fetch() internally. | |
| * We mock fetch globally (matching the pattern in helpers.test.ts) so we | |
| * control what getGeoJSONRequest returns without partially mocking the module. | |
| * | |
| * Leaflet's geoJSON() and FeatureGroup are used on the success path. | |
| * We mock leaflet (same style as PagePredictionMap.test.ts) so those | |
| * constructors don't require a real DOM map. | |
| * | |
| * IMPORTANT: vi.mock is hoisted above imports, so the mock factory cannot | |
| * close over module-level variables that haven't been initialised yet. | |
| * We therefore define the mocks inside the factory using local variables, | |
| * and expose them via the module re-export so individual tests can import | |
| * the mocked functions and spy/assert on them after hoisting completes. | |
| */ | |
| vi.mock('leaflet', async () => { | |
| const actual = await vi.importActual('leaflet') | |
| return { | |
| ...actual, | |
| /** | |
| * geoJSON (aliased as LeafletGeoJSON in helpers.ts) is called as a | |
| * plain function: `const featureNew = LeafletGeoJSON(data)`. | |
| * It must return something that can be passed to map.addLayer() | |
| * and collected into a FeatureGroup array. | |
| */ | |
| geoJSON: vi.fn(() => ({ _mock: 'geoJsonLayer' })), | |
| /** | |
| * FeatureGroup is used as `new FeatureGroup([...])` so its mock must | |
| * be a real constructor (class). We use a class so vitest doesn't warn | |
| * about non-constructor vi.fn() usage. | |
| */ | |
| FeatureGroup: vi.fn(function(this: any, _layers: any[]) { | |
| this._mock = 'featureGroupInstance' | |
| }), | |
| icon: vi.fn(() => ({})), | |
| } | |
| }) | |
| vi.mock('leaflet-providers', () => ({})) | |
| vi.mock('@geoman-io/leaflet-geoman-free', () => ({})) | |
| // Imports must come after vi.mock declarations (hoisting order is fine here). | |
| import { sendMLRequest } from '@/components/helpers' | |
| import { | |
| mapNavigationLocked, | |
| layerControlGroupLayersRef, | |
| waitingString, | |
| OpenStreetMap, | |
| } from '@/components/constants' | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Helpers | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Minimal mock Leaflet map satisfying the sendMLRequest surface. | |
| * getBounds() shape matches what getExtentCurrentViewMapBBox accesses. | |
| */ | |
| const makeMockMap = (opts?: { dragEnabled?: boolean; editEnabled?: boolean }) => ({ | |
| pm: { | |
| globalDragModeEnabled: vi.fn(() => opts?.dragEnabled ?? false), | |
| globalEditModeEnabled: vi.fn(() => opts?.editEnabled ?? false), | |
| disableGlobalDragMode: vi.fn(), | |
| disableGlobalEditMode: vi.fn(), | |
| }, | |
| getZoom: vi.fn(() => 10), | |
| getBounds: vi.fn(() => ({ | |
| getNorthEast: () => ({ lat: 45, lng: 10 }), | |
| getSouthWest: () => ({ lat: 44, lng: 9 }), | |
| })), | |
| addLayer: vi.fn(), | |
| }) | |
| const samplePrompts = [ | |
| { id: 1, type: 'point' as const, data: { lat: 45, lng: 10 }, label: 1 }, | |
| ] | |
| /** Minimal valid GeoJSON response shaped as getGeoJSONRequest expects */ | |
| const validParsedOutput = { | |
| duration_run: 0.5, | |
| output: { | |
| geojson: '{"type":"FeatureCollection","features":[]}', | |
| n_predictions: 1, | |
| n_shapes_geojson: 1, | |
| }, | |
| } | |
| const make200Response = (parsedOutput: object) => ({ | |
| status: 200, | |
| json: () => Promise.resolve({ body: JSON.stringify(parsedOutput) }), | |
| }) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // T12: disables drag/edit modes and locks navigation | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('T12: sendMLRequest disables drag/edit modes and locks navigation', () => { | |
| beforeEach(() => { | |
| mapNavigationLocked.value = false | |
| vi.spyOn(console, 'error').mockImplementation(() => {}) | |
| }) | |
| afterEach(() => { | |
| vi.restoreAllMocks() | |
| }) | |
| it('disables drag mode when globalDragModeEnabled is true', async () => { | |
| const mockMap = makeMockMap({ dragEnabled: true, editEnabled: true }) | |
| vi.spyOn(global, 'fetch').mockRejectedValue(new Error('network error')) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(mockMap.pm.disableGlobalDragMode).toHaveBeenCalledTimes(1) | |
| }) | |
| it('disables edit mode when globalEditModeEnabled is true', async () => { | |
| const mockMap = makeMockMap({ dragEnabled: true, editEnabled: true }) | |
| vi.spyOn(global, 'fetch').mockRejectedValue(new Error('network error')) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(mockMap.pm.disableGlobalEditMode).toHaveBeenCalledTimes(1) | |
| }) | |
| it('sets mapNavigationLocked to true', async () => { | |
| const mockMap = makeMockMap({ dragEnabled: true, editEnabled: true }) | |
| vi.spyOn(global, 'fetch').mockRejectedValue(new Error('network error')) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(mapNavigationLocked.value).toBe(true) | |
| }) | |
| }) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // T13: builds correct request body | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('T13: sendMLRequest builds correct request body', () => { | |
| afterEach(() => { | |
| vi.restoreAllMocks() | |
| }) | |
| it('sends fetch with bbox, prompt, zoom, and source_type', async () => { | |
| const mockMap = makeMockMap() | |
| const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(make200Response(validParsedOutput) as any) | |
| await sendMLRequest(mockMap as any, samplePrompts, OpenStreetMap) | |
| expect(fetchSpy).toHaveBeenCalledOnce() | |
| const [url, init] = fetchSpy.mock.calls[0] | |
| expect(url).toBe('/infer_samgis') | |
| const body = JSON.parse(init!.body as string) | |
| expect(body.bbox).toEqual({ | |
| ne: { lat: 45, lng: 10 }, | |
| sw: { lat: 44, lng: 9 }, | |
| }) | |
| expect(body.prompt).toEqual(samplePrompts) | |
| expect(body.zoom).toBe(10) | |
| expect(body.source_type).toBe(OpenStreetMap) | |
| }) | |
| }) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // T14: success β adds GeoJSON layer to map and calls addOverlay | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('T14: sendMLRequest success adds GeoJSON layer to map', () => { | |
| const mockAddOverlay = vi.fn() | |
| beforeEach(() => { | |
| mockAddOverlay.mockReset() | |
| layerControlGroupLayersRef.value = { addOverlay: mockAddOverlay } as any | |
| }) | |
| afterEach(() => { | |
| vi.restoreAllMocks() | |
| }) | |
| it('calls leafletMap.addLayer with the geoJSON layer', async () => { | |
| const mockMap = makeMockMap() | |
| vi.spyOn(global, 'fetch').mockResolvedValue(make200Response(validParsedOutput) as any) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| // geoJSON mock returns { _mock: 'geoJsonLayer' } β addLayer should receive it | |
| expect(mockMap.addLayer).toHaveBeenCalledTimes(1) | |
| const layerArg = mockMap.addLayer.mock.calls[0][0] | |
| expect(layerArg).toHaveProperty('_mock', 'geoJsonLayer') | |
| }) | |
| it('calls layerControlGroupLayersRef.addOverlay with a FeatureGroup', async () => { | |
| const mockMap = makeMockMap() | |
| vi.spyOn(global, 'fetch').mockResolvedValue(make200Response(validParsedOutput) as any) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(mockAddOverlay).toHaveBeenCalledOnce() | |
| // Second arg is the locale timestamp string | |
| expect(typeof mockAddOverlay.mock.calls[0][1]).toBe('string') | |
| // First arg is a FeatureGroup instance constructed by the mock | |
| const overlayArg = mockAddOverlay.mock.calls[0][0] | |
| expect(overlayArg).toHaveProperty('_mock', 'featureGroupInstance') | |
| }) | |
| }) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // T15: error β logs and does not throw | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('T15: sendMLRequest error logs and does not throw', () => { | |
| afterEach(() => { | |
| vi.restoreAllMocks() | |
| }) | |
| it('does not throw when fetch rejects', async () => { | |
| const mockMap = makeMockMap() | |
| vi.spyOn(global, 'fetch').mockRejectedValue(new Error('network failure')) | |
| vi.spyOn(console, 'error').mockImplementation(() => {}) | |
| await expect(sendMLRequest(mockMap as any, samplePrompts)).resolves.not.toThrow() | |
| }) | |
| it('calls console.error at least once on fetch failure', async () => { | |
| const mockMap = makeMockMap() | |
| vi.spyOn(global, 'fetch').mockRejectedValue(new Error('network failure')) | |
| const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(errorSpy).toHaveBeenCalled() | |
| }) | |
| }) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // T16: skips disable when drag/edit not enabled | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('T16: sendMLRequest skips disable when drag/edit not enabled', () => { | |
| afterEach(() => { | |
| vi.restoreAllMocks() | |
| }) | |
| it('does NOT call disableGlobalDragMode when drag mode is off', async () => { | |
| const mockMap = makeMockMap({ dragEnabled: false, editEnabled: false }) | |
| vi.spyOn(global, 'fetch').mockResolvedValue(make200Response(validParsedOutput) as any) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(mockMap.pm.disableGlobalDragMode).not.toHaveBeenCalled() | |
| }) | |
| it('does NOT call disableGlobalEditMode when edit mode is off', async () => { | |
| const mockMap = makeMockMap({ dragEnabled: false, editEnabled: false }) | |
| vi.spyOn(global, 'fetch').mockResolvedValue(make200Response(validParsedOutput) as any) | |
| await sendMLRequest(mockMap as any, samplePrompts) | |
| expect(mockMap.pm.disableGlobalEditMode).not.toHaveBeenCalled() | |
| }) | |
| }) | |