Files
Starseed/frontend/modules/commercial/components/__tests__/TierAddressMap.spec.ts
T
Matthieu d16c7e5541 feat(field_sales) : onglet Carte fiches Client/Fournisseur + point de départ par site ou adresse
Onglet « Carte » sur les fiches Client et Fournisseur (visible sous
field_sales.tours.view et module actif) : tous les points géolocalisés du
Tiers sur une carte Leaflet, pin ajustable (PATCH lat/lng + geoManual),
adresses non géolocalisées listées à part. Composant réutilisable
TierAddressMap.

Écran de planification : point de départ choisi parmi les sites de
l'utilisateur (MalioSelect) ou en adresse libre (autocomplete BAN), marqueur
« maison » sur la carte et trace partant du départ. Le mode n'est dérivé du
back qu'au premier chargement (les sauvegardes ne réécrasent plus le choix).

Suppression de l'ajout d'étape « point libre » (bouton + modale) ; l'affichage
des étapes custom existantes est conservé.
2026-06-12 08:56:37 +02:00

159 lines
5.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import TierAddressMap, { type TierMapAddress } from '../TierAddressMap.vue'
// Mock Leaflet (hoisted) : capture les marqueurs crees (un par adresse
// geolocalisee) et leur handler `dragend`, et trace l'appel a fitBounds.
const leafletState = vi.hoisted(() => ({
markers: [] as Array<{
_latlng: { lat: number, lng: number }
dragend: (() => void) | null
setLatLng: ReturnType<typeof vi.fn>
}>,
fitBoundsCalled: false,
}))
vi.mock('leaflet', () => {
function makeMarker(lat: number, lng: number) {
const marker = {
_latlng: { lat, lng },
dragend: null as (() => void) | null,
addTo: vi.fn().mockReturnThis(),
bindPopup: vi.fn().mockReturnThis(),
on: vi.fn((event: string, handler: () => void) => {
if (event === 'dragend') marker.dragend = handler
}),
getLatLng: vi.fn(() => marker._latlng),
setLatLng: vi.fn(),
remove: vi.fn(),
}
return marker
}
const map = {
fitBounds: vi.fn(() => { leafletState.fitBoundsCalled = true }),
setView: vi.fn().mockReturnThis(),
remove: vi.fn(),
}
const L = {
map: vi.fn(() => map),
tileLayer: vi.fn(() => ({ addTo: vi.fn() })),
divIcon: vi.fn(() => ({})),
latLngBounds: vi.fn((points: unknown) => points),
marker: vi.fn((pos: [number, number]) => {
const marker = makeMarker(pos[0], pos[1])
leafletState.markers.push(marker)
return marker
}),
}
return { default: L, ...L }
})
vi.mock('leaflet/dist/leaflet.css', () => ({ default: {} }))
// Mock controlable de l'API (PATCH des coordonnees au drag).
const { patchMock } = vi.hoisted(() => ({ patchMock: vi.fn() }))
// Auto-imports Nuxt/Vue utilises sans import explicite par le composant.
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
vi.stubGlobal('useApi', () => ({ patch: patchMock }))
vi.stubGlobal('ref', ref)
vi.stubGlobal('computed', computed)
vi.stubGlobal('watch', watch)
vi.stubGlobal('nextTick', nextTick)
vi.stubGlobal('onMounted', onMounted)
vi.stubGlobal('onBeforeUnmount', onBeforeUnmount)
function address(over: Partial<TierMapAddress> = {}): TierMapAddress {
return {
id: 1,
latitude: '47.218',
longitude: '-1.553',
geoManual: false,
title: '1 rue du Test, 44000 Nantes',
typeLabel: 'Livraison',
patchPath: '/client_addresses/1',
...over,
}
}
beforeEach(() => {
leafletState.markers = []
leafletState.fitBoundsCalled = false
patchMock.mockReset()
patchMock.mockResolvedValue({})
})
describe('TierAddressMap — marqueurs', () => {
it('pose un marqueur par adresse geolocalisee et liste a part celles sans coordonnees', async () => {
const wrapper = mount(TierAddressMap, {
props: {
addresses: [
address({ id: 1, patchPath: '/client_addresses/1' }),
address({ id: 2, latitude: '48.85', longitude: '2.35', patchPath: '/client_addresses/2' }),
address({ id: 3, latitude: null, longitude: null, patchPath: '/client_addresses/3', title: '5 rue Sans Geo' }),
],
},
})
await flushPromises() // import dynamique de Leaflet + montage carte
// Deux adresses geolocalisees -> deux marqueurs ; la troisieme (sans
// coords) n'est pas posee sur la carte mais listee a part.
expect(leafletState.markers).toHaveLength(2)
expect(leafletState.fitBoundsCalled).toBe(true)
const missing = wrapper.findAll('[data-testid="tier-map-missing"]')
expect(missing).toHaveLength(1)
expect(missing[0]?.text()).toContain('5 rue Sans Geo')
expect(wrapper.find('[data-testid="tier-map"]').exists()).toBe(true)
})
it('affiche un etat vide quand aucune adresse n\'est geolocalisee', async () => {
const wrapper = mount(TierAddressMap, {
props: { addresses: [address({ latitude: null, longitude: null })] },
})
await flushPromises()
expect(leafletState.markers).toHaveLength(0)
expect(wrapper.find('[data-testid="tier-map"]').exists()).toBe(false)
expect(wrapper.findAll('[data-testid="tier-map-missing"]')).toHaveLength(1)
})
})
describe('TierAddressMap — pin ajustable (RG-6.08)', () => {
it('PATCH les coordonnees + geoManual=true au drag quand editable', async () => {
const wrapper = mount(TierAddressMap, {
props: { addresses: [address({ id: 7, patchPath: '/client_addresses/7' })], editable: true },
})
await flushPromises()
const marker = leafletState.markers[0]
expect(marker?.dragend).not.toBeNull()
// L'utilisateur depose le pin ailleurs (entree de site mal geocodee).
marker!._latlng = { lat: 48.1234567, lng: -1.6543217 }
marker!.dragend?.()
await flushPromises()
expect(patchMock).toHaveBeenCalledWith(
'/client_addresses/7',
{ latitude: '48.1234567', longitude: '-1.6543217', geoManual: true },
{ toast: false },
)
expect(wrapper.emitted('updated')?.[0]?.[0]).toEqual({
id: 7,
latitude: '48.1234567',
longitude: '-1.6543217',
})
})
it('ne rend pas les marqueurs draggables (pas de PATCH) en lecture seule', async () => {
mount(TierAddressMap, {
props: { addresses: [address()], editable: false },
})
await flushPromises()
// Aucun handler dragend cable -> pas de drag possible.
expect(leafletState.markers[0]?.dragend).toBeNull()
})
})