From e06bc79127c1e9f00ac6c7bcad9b493f9633a4e3 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 8 Jun 2026 12:29:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(address)=20:=20recherche=20adresse=20BAN=20?= =?UTF-8?q?=E2=80=94=20retry=20apres=20erreur=20+=20garde=203=20caracteres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Une erreur de l'API d'autocompletion (BAN) ne bascule plus le champ Adresse en saisie libre de maniere definitive : l'autocompletion reste montee et chaque frappe relance la recherche (le flag degrade etait verrouille a true sans jamais etre reinitialise). - Garde min. 3 caracteres avant l'appel BAN (evite le 400 de l'API). - Ville : repli saisie libre conserve mais recuperable (re-saisir le code postal repasse en select au succes). - Avertissement « service indisponible » emis une seule fois. - Tests Vitest : pas d'appel < 3 car., relance apres erreur, emission unique de l'evenement. --- .../components/ClientAddressBlock.vue | 49 ++++++++----- .../__tests__/ClientAddressBlock.spec.ts | 73 +++++++++++++++++-- 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/frontend/modules/commercial/components/ClientAddressBlock.vue b/frontend/modules/commercial/components/ClientAddressBlock.vue index 1745a4b..a188159 100644 --- a/frontend/modules/commercial/components/ClientAddressBlock.vue +++ b/frontend/modules/commercial/components/ClientAddressBlock.vue @@ -87,8 +87,9 @@ @update:model-value="onPostalCodeChange" /> - +
- + ([]) // Adresses proposees par la BAN (alimentees a la saisie d'adresse). @@ -258,10 +266,10 @@ function update(field: K, value: AddressFormDr emit('update:modelValue', { ...props.modelValue, [field]: value }) } -/** Bascule définitivement en mode degrade et previent le parent (toast unique). */ -function enterDegraded(): void { - if (!degraded.value) { - degraded.value = true +/** Previent le parent (toast unique) que l'autocompletion est indisponible. */ +function notifyUnavailable(): void { + if (!unavailableNotified) { + unavailableNotified = true emit('degraded') } } @@ -270,9 +278,6 @@ function enterDegraded(): void { async function onPostalCodeChange(value: string): Promise { update('postalCode', value) - if (degraded.value) { - return - } const digits = (value ?? '').replace(/\D/g, '') if (digits.length < 5) { return @@ -280,15 +285,22 @@ async function onPostalCodeChange(value: string): Promise { try { const suggestions = await autocomplete.searchCity(digits) banCityOptions.value = suggestions.map(s => ({ value: s.city, label: s.city })) + // Service repondu : on (re)passe la Ville en select assiste. + degraded.value = false } catch { - enterDegraded() + // BAN indispo : Ville en saisie libre (recuperable au prochain essai). + degraded.value = true + notifyUnavailable() } } /** Recherche d'adresse assistee (event de MalioInputAutocomplete). */ async function onAddressSearch(query: string): Promise { - if (degraded.value) { + // La BAN exige au moins 3 caracteres : on n'envoie rien en deca (evite un 400) + // et on vide les suggestions devenues obsoletes. + if (query.trim().length < 3) { + banAddressOptions.value = [] return } addressLoading.value = true @@ -299,7 +311,10 @@ async function onAddressSearch(query: string): Promise { banAddressOptions.value = suggestions.map(s => ({ value: s.street, label: s.label })) } catch { - enterDegraded() + // Erreur transitoire : on vide les suggestions, la prochaine frappe reessaie + // (pas de bascule definitive — c'etait le bug). Avertissement une seule fois. + banAddressOptions.value = [] + notifyUnavailable() } finally { addressLoading.value = false diff --git a/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts b/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts index 6f0ba9e..20cb438 100644 --- a/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts +++ b/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts @@ -1,16 +1,21 @@ -import { describe, it, expect, vi } from 'vitest' -import { mount } from '@vue/test-utils' +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' -// Le composable BAN est mocke : aucun appel reseau, aucune suggestion chargee. -// On reproduit ainsi l'etat « adresse persistee, mais liste de suggestions -// vide » (remontage apres validation / edition d'une adresse existante). +// 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: vi.fn(), - searchAddress: vi.fn(), + searchCity: searchCityMock, + searchAddress: searchAddressMock, }), })) @@ -130,3 +135,57 @@ describe('ClientAddressBlock — mapping erreur par champ (ERP-101)', () => { 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) + }) +})