import { beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import { nextTick } from 'vue' import { createPinia, setActivePinia } from 'pinia' import ProvenanceMap from '@/components/provenance/ProvenanceMap.vue' import vuetify from '@/plugins/vuetify' const leaflet = vi.hoisted(() => { const markers = [] const polylines = [] const layers = [] function makeLayer (type) { const layer = { type, addTo: vi.fn(function () { return this }), on: vi.fn(), bindTooltip: vi.fn(function () { return this }), bindPopup: vi.fn(function () { return this }), setPopupContent: vi.fn(), openPopup: vi.fn(), remove: vi.fn(), getLatLng: vi.fn(function () { return this.pos }), setLatLng: vi.fn(), } layers.push(layer) return layer } const map = { setView: vi.fn(function () { return this }), fitBounds: vi.fn(), flyTo: vi.fn(), remove: vi.fn(), invalidateSize: vi.fn(), on: vi.fn(), getZoom: vi.fn(() => 8), } const bounds = { extend: vi.fn(), isValid: vi.fn(() => true) } return { markers, polylines, layers, map, bounds, L: { map: vi.fn(() => map), tileLayer: vi.fn(() => makeLayer('tileLayer')), marker: vi.fn((pos, opts) => { const marker = makeLayer('marker') marker.pos = pos marker.opts = opts markers.push(marker) return marker }), divIcon: vi.fn(opts => opts), polyline: vi.fn((points, opts) => { const polyline = makeLayer('polyline') polyline.points = points polyline.opts = opts polylines.push(polyline) return polyline }), latLngBounds: vi.fn(() => bounds), }, } }) vi.mock('leaflet', () => ({ default: leaflet.L })) const settings = { latitude: 4.6, longitude: -74.08, address: 'Cra 1 #2-3' } const provenance = [ { product: { id: 1, name: 'Panela regional', catalogue_images: ['http://localhost/media/panela.jpg'] }, suppliers: [ { supplier: { id: 5, name: 'Asociación La Mesa' }, organization: { id: 3, name: 'Red de Economía Solidaria' }, municipality: { id: 7, name: 'Santa Bárbara', latitude: 6.23, longitude: -75.56 }, }, ], }, { product: { id: 2, name: 'Arroz blanco', catalogue_images: [] }, suppliers: [ { supplier: { id: 6, name: 'Campesinos del Oriente' }, municipality: { id: 8, name: 'La Mesa', latitude: 4.86, longitude: -74.63 } }, { supplier: { id: 7, name: 'Finca El Paraíso' }, municipality: { id: 9, name: 'San Antonio', latitude: 6.22, longitude: -75.57 } }, ], }, ] const sameMuni = [ { product: { id: 1, name: 'Panela', catalogue_images: [] }, suppliers: [{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'LA MESA', latitude: '4.6310280', longitude: '-74.4615880' } }], }, { product: { id: 2, name: 'Arroz', catalogue_images: [] }, suppliers: [{ supplier: { id: 6, name: 'B' }, municipality: { id: 7, name: 'LA MESA', latitude: '4.6310280', longitude: '-74.4615880' } }], }, ] function mountMap (props = {}) { const api = { getStoreSettings: vi.fn().mockResolvedValue(settings) } const pinia = createPinia() setActivePinia(pinia) const wrapper = mount(ProvenanceMap, { props: { provenance, ...props }, global: { plugins: [pinia, vuetify], provide: { api }, }, }) return { api, wrapper } } function handlerOf (marker, event) { return marker.on.mock.calls.find(args => args[0] === event)[1] } beforeEach(() => { vi.clearAllMocks() leaflet.markers.length = 0 leaflet.polylines.length = 0 leaflet.layers.length = 0 document.body.innerHTML = '' }) describe('ProvenanceMap', () => { it('carga la configuración de la tienda al montarse', async () => { const { api } = mountMap() await flushPromises() expect(api.getStoreSettings).toHaveBeenCalled() }) it('crea el mapa con la tienda y un marcador por municipio de origen', async () => { mountMap() await flushPromises() expect(leaflet.L.map).toHaveBeenCalledTimes(1) expect(leaflet.L.tileLayer).toHaveBeenCalledTimes(1) expect(leaflet.markers).toHaveLength(4) expect(leaflet.map.fitBounds).toHaveBeenCalled() expect(leaflet.markers[0].bindTooltip).toHaveBeenCalled() }) it('al pasar sobre un producto dibuja una línea hasta la tienda', async () => { mountMap() await flushPromises() handlerOf(leaflet.markers[1], 'mouseover')() expect(leaflet.L.polyline).toHaveBeenCalledTimes(1) expect(leaflet.L.polyline.mock.calls[0][0]).toEqual([[6.23, -75.56], [4.6, -74.08]]) expect(leaflet.polylines[0].addTo).toHaveBeenCalled() }) it('al pasar sobre la tienda dibuja las líneas de todos los productos', async () => { mountMap() await flushPromises() handlerOf(leaflet.markers[0], 'mouseover')() expect(leaflet.L.polyline).toHaveBeenCalledTimes(3) }) it('al salir del marcador se limpian las líneas', async () => { mountMap() await flushPromises() handlerOf(leaflet.markers[1], 'mouseover')() expect(leaflet.polylines).toHaveLength(1) handlerOf(leaflet.markers[1], 'mouseout')() expect(leaflet.polylines[0].remove).toHaveBeenCalled() }) it('muestra el detalle del producto en un popup sobre el marcador', async () => { mountMap() await flushPromises() const marker = leaflet.markers[1] expect(marker.bindPopup).toHaveBeenCalledTimes(1) const popupEl = marker.bindPopup.mock.calls[0][0] expect(popupEl.textContent).toContain('Panela regional') expect(popupEl.textContent).toContain('Asociación La Mesa') expect(popupEl.textContent).toContain('Santa Bárbara') }) it('no abre el modal web para productos con ubicación', async () => { mountMap() await flushPromises() expect(document.body.textContent).not.toContain('Proveedor') }) it('no dibuja la línea de la tienda cuando la tienda no tiene coordenadas', async () => { const api = { getStoreSettings: vi.fn().mockResolvedValue({ address: 'Cra 1' }) } const pinia = createPinia() setActivePinia(pinia) mount(ProvenanceMap, { props: { provenance }, global: { plugins: [pinia, vuetify], provide: { api }, }, }) await flushPromises() expect(leaflet.markers).toHaveLength(3) handlerOf(leaflet.markers[0], 'mouseover')() expect(leaflet.L.polyline).not.toHaveBeenCalled() }) it('muestra un aviso cuando ningún municipio tiene coordenadas', async () => { const noCoords = [ { product: { id: 1, name: 'Panela' }, suppliers: [{ supplier: { id: 5, name: 'A' }, municipality: { id: 7, name: 'M' } }] }, ] const { wrapper } = mountMap({ provenance: noCoords }) await flushPromises() expect(leaflet.L.map).not.toHaveBeenCalled() expect(wrapper.text()).toContain('coordenadas') }) it('mantiene la posición exacta del municipio aunque varios productos coincidan', async () => { mountMap({ provenance: sameMuni }) await flushPromises() expect(leaflet.markers).toHaveLength(2) expect(leaflet.markers[1].pos).toEqual([4.631028, -74.461588]) expect(leaflet.map.on).not.toHaveBeenCalledWith('zoomend', expect.anything()) }) it('agrupa los productos del mismo punto en un marcador con contador', async () => { mountMap({ provenance: sameMuni }) await flushPromises() const groupIcon = leaflet.L.divIcon.mock.calls.find(args => args[0].className === 'provenance-group-pin') expect(groupIcon).toBeDefined() expect(groupIcon[0].html).toContain('2') expect(groupIcon[0].html).toContain('mdi-package-variant') const groupMarker = leaflet.markers[1] expect(groupMarker.bindPopup).toHaveBeenCalled() expect(groupMarker.bindTooltip.mock.calls[0][0]).toContain('2 productos') }) it('al hacer clic en el marcador agrupado despliega los productos internos', async () => { mountMap({ provenance: sameMuni }) await flushPromises() const popupEl = leaflet.markers[1].bindPopup.mock.calls[0][0] expect(popupEl.textContent).toContain('Panela') expect(popupEl.textContent).toContain('Arroz') }) it('al hacer clic en un producto del popup agrupado muestra su detalle en el marcador', async () => { mountMap({ provenance: sameMuni }) await flushPromises() const groupMarker = leaflet.markers[1] const popupEl = groupMarker.bindPopup.mock.calls[0][0] const item = popupEl.querySelector('[data-test="map-popup-item-1"]') item.dispatchEvent(new Event('click')) expect(groupMarker.setPopupContent).toHaveBeenCalledTimes(1) const detailEl = groupMarker.setPopupContent.mock.calls[0][0] expect(detailEl.textContent).toContain('Arroz') expect(detailEl.textContent).toContain('B') expect(groupMarker.openPopup).toHaveBeenCalled() }) it('al pasar sobre la tienda dibuja una línea por cada producto agrupado', async () => { mountMap({ provenance: sameMuni }) await flushPromises() handlerOf(leaflet.markers[0], 'mouseover')() expect(leaflet.L.polyline).toHaveBeenCalledTimes(2) }) it('al pasar sobre el marcador agrupado dibuja las líneas de sus productos', async () => { mountMap({ provenance: sameMuni }) await flushPromises() handlerOf(leaflet.markers[1], 'mouseover')() expect(leaflet.L.polyline).toHaveBeenCalledTimes(2) expect(leaflet.polylines[0].points[0]).toEqual([4.631028, -74.461588]) }) it('muestra un recuadro con todos los productos, incluidos los sin geolocalización', async () => { const withNoGeo = [ { product: { id: 1, name: 'Panela regional', catalogue_images: ['http://localhost/media/panela.jpg'] }, suppliers: [{ supplier: { id: 5, name: 'Asociación La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara', latitude: 6.23, longitude: -75.56 } }], }, { product: { id: 2, name: 'Café de montaña', catalogue_images: [] }, suppliers: [{ supplier: { id: 8, name: 'Caficultores del Valle' }, municipality: null }], }, { product: { id: 3, name: 'Miel pura', catalogue_images: [] }, suppliers: [], }, ] const { wrapper } = mountMap({ provenance: withNoGeo }) await flushPromises() const rows = wrapper.findAll('[data-test="map-product-row"]') expect(rows).toHaveLength(3) expect(rows.map(row => row.text().replace(/\s+/g, ' ').trim())).toEqual([ expect.stringContaining('Panela regional'), expect.stringContaining('Café de montaña'), expect.stringContaining('Miel pura'), ]) expect(rows[1].text()).toContain('Sin geolocalización') expect(rows[2].text()).toContain('Sin geolocalización') expect(rows[0].text()).toContain('Santa Bárbara') }) it('al hacer clic en un producto con ubicación centra el mapa y abre su detalle en el marcador', async () => { const { wrapper } = mountMap() await flushPromises() await wrapper.find('[data-test="map-product-row"]').trigger('click') await nextTick() expect(leaflet.map.flyTo).toHaveBeenCalledTimes(1) expect(leaflet.map.flyTo.mock.calls[0][0]).toEqual([6.23, -75.56]) const marker = leaflet.markers[1] expect(marker.setPopupContent).toHaveBeenCalled() expect(marker.setPopupContent.mock.calls[0][0].textContent).toContain('Panela regional') expect(marker.setPopupContent.mock.calls[0][0].textContent).toContain('Asociación La Mesa') expect(marker.openPopup).toHaveBeenCalled() expect(document.body.textContent).not.toContain('Proveedor') }) it('al hacer clic en un producto sin geolocalización abre el diálogo con la información disponible sin centrar el mapa', async () => { const withNoGeo = [ { product: { id: 1, name: 'Panela regional', catalogue_images: [] }, suppliers: [{ supplier: { id: 5, name: 'Asociación La Mesa' }, municipality: { id: 7, name: 'Santa Bárbara', latitude: 6.23, longitude: -75.56 } }], }, { product: { id: 2, name: 'Café de montaña', catalogue_images: [] }, suppliers: [{ supplier: { id: 8, name: 'Caficultores del Valle' }, municipality: null }], }, ] const { wrapper } = mountMap({ provenance: withNoGeo }) await flushPromises() const noGeoRow = wrapper.findAll('[data-test="map-product-row"]').find(row => row.text().includes('Café de montaña')) await noGeoRow.trigger('click') await nextTick() expect(leaflet.map.flyTo).not.toHaveBeenCalled() expect(document.body.textContent).toContain('Café de montaña') expect(document.body.textContent).toContain('Caficultores del Valle') expect(document.body.textContent).toContain('sin geolocalización') }) it('al hacer clic en el botón de zoom general vuelve a encuadrar todos los marcadores', async () => { const { wrapper } = mountMap() await flushPromises() leaflet.map.fitBounds.mockClear() await wrapper.find('[data-test="map-reset-zoom"]').trigger('click') expect(leaflet.map.fitBounds).toHaveBeenCalledTimes(1) }) it('lee las coordenadas string del backend y da fondo visible al producto sin imagen', async () => { const backendProvenance = [ { product: { id: 110, name: 'Panela condimentada 150 grs', catalogue_images: [] }, suppliers: [ { supplier: { id: 4, name: 'Asociación Agropecuaria La Mesa' }, organization: null, municipality: { id: 2653, name: 'LA MESA', latitude: '4.6310280', longitude: '-74.4615880' }, department: { id: 91, name: 'Cundinamarca' }, country: { id: 3, name: 'Colombia', code: 'CO' }, }, ], }, ] mountMap({ provenance: backendProvenance }) await flushPromises() expect(leaflet.L.map).toHaveBeenCalledTimes(1) expect(leaflet.markers).toHaveLength(2) expect(leaflet.markers[1].pos).toEqual([4.631028, -74.461588]) const productIcon = leaflet.L.divIcon.mock.calls.find(args => args[0].html.includes('provenance-product-fallback')) expect(productIcon).toBeDefined() }) })