feat(front) : mapping des erreurs de validation 422 par champ (ERP-101) (#58)
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
## 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>
This commit was merged in pull request #58.
This commit is contained in:
@@ -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('')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user