381 lines
13 KiB
JavaScript
381 lines
13 KiB
JavaScript
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 }),
|
|
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('al hacer clic en un producto abre el diálogo con el proveedor', async () => {
|
|
mountMap()
|
|
await flushPromises()
|
|
|
|
handlerOf(leaflet.markers[1], 'click')()
|
|
await nextTick()
|
|
|
|
expect(document.body.textContent).toContain('Asociación La Mesa')
|
|
expect(document.body.textContent).toContain('Panela regional')
|
|
})
|
|
|
|
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 abre el diálogo', async () => {
|
|
mountMap({ provenance: sameMuni })
|
|
await flushPromises()
|
|
|
|
const popupEl = leaflet.markers[1].bindPopup.mock.calls[0][0]
|
|
const item = popupEl.querySelector('[data-test="map-popup-item-1"]')
|
|
item.dispatchEvent(new Event('click'))
|
|
await nextTick()
|
|
|
|
expect(document.body.textContent).toContain('Arroz')
|
|
expect(document.body.textContent).toContain('B')
|
|
})
|
|
|
|
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 el diálogo', 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])
|
|
expect(document.body.textContent).toContain('Asociación La Mesa')
|
|
expect(document.body.textContent).toContain('Panela regional')
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|