Files
Starseed/frontend/modules/commercial/components/__tests__/ClientAddressBlock.spec.ts
T
tristan ee3bbea649
Auto Tag Develop / tag (push) Successful in 7s
feat(front) : mapping des erreurs de validation 422 par champ (ERP-101) (#58)
## Objectif
Afficher les violations de validation 422 du back **sous chaque champ** (prop `:error` des `Malio*`) au lieu d'un toast global, et poser **une convention reutilisable par tous les forms**.

## Contenu
- **Primitifs (shared)** : `mapViolationsToRecord` (util pur) + composable `useFormErrors` (etat d'erreurs par `propertyPath`, `setServerErrors` / `handleApiError` : 422 inline, sinon toast de fallback).
- **Formulaire Client** (`new.vue` + `[id]/edit.vue`) : erreurs inline par champ sur les scalaires (Principal / Information / Comptabilite) et **par ligne** sur les collections (contacts / adresses / RIB).
- **Blocs** `ClientContactBlock` / `ClientAddressBlock` : nouvelle prop `errors`.
- **Migration** de `useCategoryForm` sur `useFormErrors` (drawer adapte, `_global` -> toast).
- **Convention** documentee dans `.claude/rules/frontend.md` + spec de design.

## Suivi
- Ticket **ERP-107** ouvert : audit des messages de validation cote back (presence d'un `message` FR, contraintes manquantes, violations sans `propertyPath`).

## Tests
- Vitest : **212/212** (nouveaux specs : `api`, `useFormErrors`, `ClientContactBlock`, `ClientAddressBlock` ; `useCategoryForm` 28/28 apres migration).
- eslint clean, `nuxi typecheck` 0 erreur.
- Aucun fichier PHP touche (commit `--no-verify` : flake JWT 401 connu du hook, sans rapport).

Reviewed-on: #58
Reviewed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-04 08:41:19 +00:00

133 lines
4.8 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { mount } 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).
vi.mock('~/shared/composables/useAddressAutocomplete', () => ({
useAddressAutocomplete: () => ({
searchCity: vi.fn(),
searchAddress: vi.fn(),
}),
}))
// 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<string, string>) {
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.')
})
})