import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { defineComponent, h, ref, computed } from 'vue' import { emptyProviderAddress } from '~/modules/technique/types/providerForm' import ProviderAddressBlock from '../ProviderAddressBlock.vue' // Mocks controlables du composable BAN (hoisted), reutilise tel quel du M1/M2. 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 = {}, errors?: Record) { return mount(ProviderAddressBlock, { props: { modelValue: { ...emptyProviderAddress(), ...overrides }, categoryOptions: [], siteOptions: [], contactOptions: [], countryOptions: [], ...(errors ? { errors } : {}), }, global: { stubs: { MalioButtonIcon: true, MalioSelect: true, MalioSelectCheckbox: true, MalioInputText: true, MalioInputAutocomplete: MalioInputAutocompleteStub, }, }, }) } describe('ProviderAddressBlock — version simplifiee M3 (pas de type/bennes/triage)', () => { it('ne rend NI type d\'adresse, NI bennes, NI prestation de triage (difference M2)', () => { const wrapper = mountBlock() // Pas de stepper (bennes) ni de case a cocher (triage) dans le bloc M3. expect(wrapper.find('malio-input-number-stub').exists()).toBe(false) expect(wrapper.find('malio-checkbox-stub').exists()).toBe(false) // Aucun select ne porte le label « type d'adresse ». const hasAddressType = wrapper.findAll('malio-select-stub').some( el => el.attributes('label') === 'technique.providers.form.address.addressType', ) expect(hasAddressType).toBe(false) }) }) describe('ProviderAddressBlock — mapping erreur par champ (ERP-101)', () => { it('affiche les erreurs serveur sur sites et categories (RG-3.05 / RG-3.09)', () => { 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') === 'technique.providers.form.address.sites') const categoriesField = checkboxes.find(el => el.attributes('label') === 'technique.providers.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') === 'technique.providers.form.address.postalCode', ) expect(field?.attributes('error')).toBe('Code postal invalide.') }) }) describe('ProviderAddressBlock — autocompletion BAN (RG-3.06)', () => { beforeEach(() => { searchCityMock.mockReset() 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: '1 rue du Test, Châtellerault', street: '1 rue du Test', postalCode: '86100', city: 'Châtellerault' }, ]) const wrapper = mountBlock() const auto = wrapper.findComponent(MalioInputAutocompleteStub) auto.vm.$emit('search', 'rue du test') await flushPromises() auto.vm.$emit('search', 'rue du teste') await flushPromises() expect(searchAddressMock).toHaveBeenCalledTimes(2) }) it('cas degrade : la BAN echoue -> emet « degraded » une seule fois (RG-3.06)', async () => { searchAddressMock.mockRejectedValue(new Error('BAN indisponible')) const wrapper = mountBlock() const auto = wrapper.findComponent(MalioInputAutocompleteStub) auto.vm.$emit('search', 'rue du test') await flushPromises() auto.vm.$emit('search', 'rue du teste') 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: '1 rue du Test' }) const values = JSON.parse(wrapper.find('[data-testid="addr-autocomplete"]').attributes('data-options') ?? '[]') expect(values).toContain('1 rue du Test') }) })