import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { defineComponent, h, ref, computed } from 'vue' import { emptyCarrierAddress } from '~/modules/transport/types/carrierForm' import CarrierAddressBlock from '../CarrierAddressBlock.vue' /** * Tests de l'autocomplétion BAN du bloc Adresse transporteur (ERP-167) — réutilise * `useAddressAutocomplete` (M1/M2/M3). On vérifie le NOMINAL (CP → ville) et le * DÉGRADÉ (BAN indisponible → saisie libre + event `degraded`). */ const { searchCityMock, searchAddressMock } = vi.hoisted(() => ({ searchCityMock: vi.fn(), searchAddressMock: vi.fn(), })) vi.mock('~/shared/composables/useAddressAutocomplete', () => ({ useAddressAutocomplete: () => ({ searchCity: searchCityMock, searchAddress: searchAddressMock, }), })) vi.stubGlobal('useI18n', () => ({ t: (key: string) => key })) vi.stubGlobal('ref', ref) vi.stubGlobal('computed', computed) const MalioInputTextStub = defineComponent({ name: 'MalioInputText', props: { modelValue: { default: null }, label: { type: String, default: '' }, error: { type: String, default: '' } }, emits: ['update:modelValue'], setup(props) { return () => h('div', { 'data-testid': 'input-text', 'data-label': props.label, 'data-error': props.error }) }, }) const MalioSelectStub = defineComponent({ name: 'MalioSelect', props: { modelValue: { default: null }, options: { type: Array as () => { value: string }[], default: () => [] }, label: { type: String, default: '' }, error: { type: String, default: '' } }, emits: ['update:modelValue'], setup(props) { return () => h('div', { 'data-testid': 'select', 'data-label': props.label, 'data-options': JSON.stringify(props.options.map(o => o.value)) }) }, }) const MalioInputAutocompleteStub = defineComponent({ name: 'MalioInputAutocomplete', props: { modelValue: { default: null }, options: { type: Array as () => { value: string }[], default: () => [] }, loading: { 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 = {}) { return mount(CarrierAddressBlock, { props: { modelValue: { ...emptyCarrierAddress(), ...overrides }, countryOptions: [{ value: 'France', label: 'France' }], }, global: { stubs: { MalioButtonIcon: true, MalioInputText: MalioInputTextStub, MalioSelect: MalioSelectStub, MalioInputAutocomplete: MalioInputAutocompleteStub, }, }, }) } /** Récupère le composant MalioInputText d'un label donné. */ function inputTextByLabel(wrapper: ReturnType, label: string) { return wrapper.findAllComponents(MalioInputTextStub).find(c => c.props('label') === label) } describe('CarrierAddressBlock — autocomplétion ville (BAN) NOMINAL', () => { beforeEach(() => { searchCityMock.mockReset() searchAddressMock.mockReset() }) it('saisie d\'un CP à 5 chiffres → searchCity + peuple le select Ville', async () => { searchCityMock.mockResolvedValueOnce([{ city: 'Poitiers', postalCode: '86000' }]) const wrapper = mountBlock() const cp = inputTextByLabel(wrapper, 'transport.carriers.form.address.postalCode') cp?.vm.$emit('update:modelValue', '86000') await flushPromises() expect(searchCityMock).toHaveBeenCalledWith('86000') const citySelect = wrapper.findAllComponents(MalioSelectStub).find(c => c.props('label') === 'transport.carriers.form.address.city') const options = JSON.parse(citySelect?.attributes('data-options') ?? '[]') expect(options).toContain('Poitiers') expect(wrapper.emitted('degraded')).toBeUndefined() }) it('n\'interroge pas la BAN sous 5 chiffres', async () => { const wrapper = mountBlock() inputTextByLabel(wrapper, 'transport.carriers.form.address.postalCode')?.vm.$emit('update:modelValue', '860') await flushPromises() expect(searchCityMock).not.toHaveBeenCalled() }) }) describe('CarrierAddressBlock — autocomplétion DÉGRADÉE', () => { beforeEach(() => { searchCityMock.mockReset() searchAddressMock.mockReset() }) it('BAN ville indisponible → bascule en saisie libre + émet « degraded »', async () => { searchCityMock.mockRejectedValueOnce(new Error('BAN indisponible')) const wrapper = mountBlock() inputTextByLabel(wrapper, 'transport.carriers.form.address.postalCode')?.vm.$emit('update:modelValue', '86000') await flushPromises() expect(wrapper.emitted('degraded')).toHaveLength(1) // En dégradé, la Ville devient un MalioInputText (plus de MalioSelect ville). const citySelect = wrapper.findAllComponents(MalioSelectStub).find(c => c.props('label') === 'transport.carriers.form.address.city') expect(citySelect).toBeUndefined() expect(inputTextByLabel(wrapper, 'transport.carriers.form.address.city')).toBeDefined() }) it('autocomplétion adresse : pas d\'appel BAN sous 3 caractères', async () => { const wrapper = mountBlock() wrapper.findComponent(MalioInputAutocompleteStub).vm.$emit('search', 'ab') await flushPromises() expect(searchAddressMock).not.toHaveBeenCalled() }) it('autocomplétion adresse : émet « degraded » une seule fois malgré 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) }) })