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/clientForm' import ClientAddressBlock from '../ClientAddressBlock.vue' // Mocks controlables du composable BAN (hoisted) : chaque test configure le // comportement de searchCity / searchAddress (succes, rejet, rejet-puis-succes). // Par defaut ils renvoient undefined (aucune suggestion) — etat « adresse // persistee mais liste vide » couvert par les tests d'affichage. 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 recues, pour // verifier que la rue courante figure bien dans la liste (sinon le composant // Malio ne peut pas resoudre/afficher la valeur liee -> champ vide). 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 }, }, 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(street: string | null) { return mount(ClientAddressBlock, { props: { modelValue: { ...emptyAddress(), street }, title: 'Adresse', categoryOptions: [], siteOptions: [], contactOptions: [], countryOptions: [], }, global: { stubs: { MalioButtonIcon: true, MalioCheckbox: true, MalioSelect: true, MalioSelectCheckbox: true, MalioInputText: true, MalioInputAutocomplete: MalioInputAutocompleteStub, }, }, }) } describe('ClientAddressBlock — affichage de l\'adresse persistee', () => { it('inclut la rue courante dans les options de l\'autocomplete meme sans recherche BAN', () => { const wrapper = mountBlock('8 Boulevard du Port') const el = wrapper.find('[data-testid="addr-autocomplete"]') const values = JSON.parse(el.attributes('data-options') ?? '[]') expect(values).toContain('8 Boulevard du Port') }) }) /** * Stub MalioInputText qui re-expose `label` + `error` recus : permet de cibler * un champ par son libelle et de verifier l'erreur 422 propagee (ERP-101). */ const MalioInputTextProbe = defineComponent({ name: 'MalioInputTextProbe', props: { modelValue: { type: [String, Number, null], default: undefined }, error: { type: String, default: '' }, label: { type: String, default: '' }, readonly: { type: Boolean, default: false }, }, setup(props) { return () => h('div', { 'data-testid': 'addr-text', 'data-label': props.label, 'data-error': props.error, }) }, }) describe('ClientAddressBlock — mapping erreur par champ (ERP-101)', () => { function mountWithErrors(errors: Record) { return mount(ClientAddressBlock, { props: { modelValue: emptyAddress(), title: 'Adresse', categoryOptions: [], siteOptions: [], contactOptions: [], countryOptions: [], errors, }, global: { stubs: { MalioButtonIcon: true, MalioCheckbox: true, MalioSelect: true, MalioSelectCheckbox: true, MalioInputAutocomplete: MalioInputAutocompleteStub, MalioInputText: MalioInputTextProbe, }, }, }) } it('affiche l\'erreur serveur sur le champ code postal via la prop errors', () => { const wrapper = mountWithErrors({ postalCode: 'Code postal invalide.' }) const field = wrapper.findAll('[data-testid="addr-text"]').find( el => el.attributes('data-label') === 'commercial.clients.form.address.postalCode', ) expect(field?.attributes('data-error')).toBe('Code postal invalide.') }) }) describe('ClientAddressBlock — recherche adresse robuste (erreur BAN)', () => { beforeEach(() => { searchAddressMock.mockReset() }) it('n\'appelle pas la BAN en deca de 3 caracteres', async () => { const wrapper = mountBlock(null) const auto = wrapper.findComponent(MalioInputAutocompleteStub) auto.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(null) const auto = wrapper.findComponent(MalioInputAutocompleteStub) // 1er essai -> erreur BAN. auto.vm.$emit('search', 'boulevard du port') await flushPromises() expect(searchAddressMock).toHaveBeenCalledTimes(1) // 2e essai -> DOIT relancer l'appel (c'etait le bug : plus aucune recherche). auto.vm.$emit('search', 'boulevard du porte') await flushPromises() expect(searchAddressMock).toHaveBeenCalledTimes(2) // L'autocompletion reste montee (aucune bascule en saisie libre). 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(null) 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) }) })