feat(front) : mapping des erreurs de validation 422 par champ (ERP-101)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m40s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m10s

- composable useFormErrors + util mapViolationsToRecord (shared)
- formulaire Client (new + edit) : erreurs inline par champ (scalaires)
  et par ligne pour les collections (contacts / adresses / RIB)
- blocs ClientContactBlock / ClientAddressBlock : prop errors
- migration de useCategoryForm sur useFormErrors
- convention documentee dans .claude/rules/frontend.md
This commit is contained in:
2026-06-04 08:24:39 +02:00
parent e85d46a17b
commit 502d1a216b
15 changed files with 910 additions and 212 deletions
@@ -74,3 +74,59 @@ describe('ClientAddressBlock — affichage de l\'adresse persistee', () => {
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.')
})
})
@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { defineComponent, h, ref, computed } from 'vue'
import { emptyContact } from '~/modules/commercial/types/clientForm'
import ClientContactBlock from '../ClientContactBlock.vue'
// 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 d'un champ Malio qui re-expose la prop `error` recue dans un attribut
* data-* : permet de verifier que le bloc propage bien `:errors[champ]` sur le
* bon champ (ERP-101 — mapping erreur 422 par champ, par ligne de collection).
*/
function errorProbe(testid: string) {
return defineComponent({
name: `Probe-${testid}`,
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': testid, 'data-error': props.error })
},
})
}
function mountBlock(errors?: Record<string, string>) {
return mount(ClientContactBlock, {
props: {
modelValue: emptyContact(),
title: 'Contact 1',
...(errors ? { errors } : {}),
},
global: {
stubs: {
MalioButtonIcon: true,
MalioInputPhone: true,
MalioInputText: errorProbe('contact-text'),
MalioInputEmail: errorProbe('contact-email'),
},
},
})
}
describe('ClientContactBlock — mapping erreur par champ (ERP-101)', () => {
it('affiche l\'erreur serveur sur le champ email via la prop errors', () => {
const wrapper = mountBlock({ email: 'Adresse e-mail invalide.' })
const email = wrapper.find('[data-testid="contact-email"]')
expect(email.attributes('data-error')).toBe('Adresse e-mail invalide.')
})
it('laisse les champs sans erreur quand errors est absent', () => {
const wrapper = mountBlock()
const email = wrapper.find('[data-testid="contact-email"]')
expect(email.attributes('data-error')).toBe('')
})
})