Files
Starseed/frontend/modules/commercial/components/__tests__/SupplierAddressBlock.spec.ts
T

174 lines
7.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { defineComponent, h, ref, computed } from 'vue'
import { emptyAddress } from '~/modules/commercial/types/supplierForm'
import SupplierAddressBlock from '../SupplierAddressBlock.vue'
// Mocks controlables du composable BAN (hoisted).
const { searchCityMock, searchAddressMock } = vi.hoisted(() => ({
searchCityMock: vi.fn(),
searchAddressMock: vi.fn(),
}))
vi.mock('~/shared/composables/useAddressAutocomplete', () => ({
useAddressAutocomplete: () => ({
searchCity: searchCityMock,
searchAddress: searchAddressMock,
}),
}))
// Auto-imports Nuxt/Vue utilises sans import explicite par le composant.
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
vi.stubGlobal('ref', ref)
vi.stubGlobal('computed', computed)
// Stub de MalioInputAutocomplete : expose les `value` des options + allowCreate.
const MalioInputAutocompleteStub = defineComponent({
name: 'MalioInputAutocomplete',
props: {
modelValue: { type: [String, Number, null], default: undefined },
options: { type: Array as () => { value: string | number, label: string }[], default: () => [] },
loading: { type: Boolean, default: false },
minSearchLength: { type: Number, default: 0 },
label: { type: String, default: '' },
readonly: { type: Boolean, default: false },
allowCreate: { type: Boolean, default: false },
},
emits: ['update:modelValue', 'search', 'select'],
setup(props) {
return () => h('div', {
'data-testid': 'addr-autocomplete',
'data-options': JSON.stringify(props.options.map(o => o.value)),
})
},
})
function mountBlock(overrides: Record<string, unknown> = {}, errors?: Record<string, string>) {
return mount(SupplierAddressBlock, {
props: {
modelValue: { ...emptyAddress(), ...overrides },
title: 'Adresse 1',
categoryOptions: [],
siteOptions: [],
contactOptions: [],
countryOptions: [],
...(errors ? { errors } : {}),
},
global: {
stubs: {
MalioButtonIcon: true,
MalioCheckbox: true,
MalioRadioButton: true,
MalioInputNumber: true,
MalioSelect: true,
MalioSelectCheckbox: true,
MalioInputText: true,
MalioInputAutocomplete: MalioInputAutocompleteStub,
},
},
})
}
describe('SupplierAddressBlock — specificites M2 (radio type, bennes, triage)', () => {
it('rend les 3 options de type d\'adresse (Prospect / Départ / Rendu)', () => {
const wrapper = mountBlock()
expect(wrapper.findAll('malio-radio-button-stub')).toHaveLength(3)
})
it('rend le stepper Bennes et la case Prestation de triage (champs specifiques fournisseur)', () => {
const wrapper = mountBlock()
expect(wrapper.find('malio-input-number-stub').exists()).toBe(true)
expect(wrapper.find('malio-checkbox-stub').exists()).toBe(true)
})
it('ne rend aucun champ d\'email de facturation (difference M1)', () => {
const wrapper = mountBlock()
// Aucun MalioInputEmail dans le bloc adresse fournisseur.
expect(wrapper.find('malio-input-email-stub').exists()).toBe(false)
})
})
describe('SupplierAddressBlock — mapping erreur par champ (ERP-101)', () => {
it('affiche l\'erreur serveur du type d\'adresse (propertyPath addressType)', () => {
const wrapper = mountBlock({}, { addressType: 'Le type d\'adresse doit être Prospect, Départ ou Rendu.' })
expect(wrapper.text()).toContain('Le type d\'adresse doit être Prospect, Départ ou Rendu.')
})
it('affiche les erreurs serveur sur sites et categories', () => {
const wrapper = mountBlock({}, {
sites: 'Au moins un site est obligatoire.',
categories: 'Au moins une catégorie est obligatoire.',
})
const checkboxes = wrapper.findAll('malio-select-checkbox-stub')
const sitesField = checkboxes.find(el => el.attributes('label') === 'commercial.suppliers.form.address.sites')
const categoriesField = checkboxes.find(el => el.attributes('label') === 'commercial.suppliers.form.address.categories')
expect(sitesField?.attributes('error')).toBe('Au moins un site est obligatoire.')
expect(categoriesField?.attributes('error')).toBe('Au moins une catégorie est obligatoire.')
})
it('affiche l\'erreur serveur sur le code postal', () => {
const wrapper = mountBlock({}, { postalCode: 'Code postal invalide.' })
const field = wrapper.findAll('malio-input-text-stub').find(
el => el.attributes('label') === 'commercial.suppliers.form.address.postalCode',
)
expect(field?.attributes('error')).toBe('Code postal invalide.')
})
})
describe('SupplierAddressBlock — autocompletion adresse (BAN) robuste', () => {
beforeEach(() => {
searchAddressMock.mockReset()
})
it('n\'appelle pas la BAN en deca de 3 caracteres', async () => {
const wrapper = mountBlock()
wrapper.findComponent(MalioInputAutocompleteStub).vm.$emit('search', 'ab')
await flushPromises()
expect(searchAddressMock).not.toHaveBeenCalled()
})
it('relance la recherche apres une erreur (pas de bascule definitive)', async () => {
searchAddressMock
.mockRejectedValueOnce(new Error('BAN indisponible'))
.mockResolvedValueOnce([
{ label: '8 Boulevard du Port, Paris', street: '8 Boulevard du Port', postalCode: '75001', city: 'Paris' },
])
const wrapper = mountBlock()
const auto = wrapper.findComponent(MalioInputAutocompleteStub)
auto.vm.$emit('search', 'boulevard du port')
await flushPromises()
auto.vm.$emit('search', 'boulevard du porte')
await flushPromises()
expect(searchAddressMock).toHaveBeenCalledTimes(2)
expect(wrapper.find('[data-testid="addr-autocomplete"]').exists()).toBe(true)
})
it('emet « degraded » une seule fois malgre plusieurs erreurs', async () => {
searchAddressMock.mockRejectedValue(new Error('BAN indisponible'))
const wrapper = mountBlock()
const auto = wrapper.findComponent(MalioInputAutocompleteStub)
auto.vm.$emit('search', 'rue de la paix')
await flushPromises()
auto.vm.$emit('search', 'rue de la paixx')
await flushPromises()
expect(wrapper.emitted('degraded')).toHaveLength(1)
})
it('active allow-create sur le champ Adresse (saisie manuelle libre)', () => {
const wrapper = mountBlock()
expect(wrapper.findComponent(MalioInputAutocompleteStub).props('allowCreate')).toBe(true)
})
it('inclut la rue courante dans les options meme sans recherche BAN', () => {
const wrapper = mountBlock({ street: '8 Boulevard du Port' })
const values = JSON.parse(wrapper.find('[data-testid="addr-autocomplete"]').attributes('data-options') ?? '[]')
expect(values).toContain('8 Boulevard du Port')
})
})