samgis / static /tests /sendMLRequest.test.ts
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()
})
})