feat(front) : mapping des erreurs de validation 422 par champ (ERP-101)
- 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:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import type { Category, CategoryType } from '~/modules/catalog/types/category'
|
||||
import { useFormErrors } from '~/shared/composables/useFormErrors'
|
||||
import { useCategoryForm } from '../useCategoryForm'
|
||||
|
||||
// Stubs des auto-imports Nuxt consommes par le composable.
|
||||
@@ -21,6 +22,9 @@ vi.stubGlobal('useToast', () => ({
|
||||
success: mockToastSuccess,
|
||||
error: mockToastError,
|
||||
}))
|
||||
// useFormErrors est un auto-import Nuxt : on expose l'implementation reelle
|
||||
// (elle consomme useToast, deja stubbe ci-dessus) pour tester l'integration.
|
||||
vi.stubGlobal('useFormErrors', useFormErrors)
|
||||
// useI18n.t : on renvoie la cle telle quelle (pratique pour asserter dessus).
|
||||
// Quand le composable passe des params (ex: doublon), on les serialise pour
|
||||
// pouvoir verifier que l'interpolation a bien recu le bon nom.
|
||||
@@ -61,7 +65,7 @@ describe('useCategoryForm', () => {
|
||||
|
||||
expect(form.name.value).toBe('Vis')
|
||||
expect(form.categoryTypeId.value).toBe(1)
|
||||
expect(form.errors.value).toEqual({ name: '', categoryType: '', _global: '' })
|
||||
expect(form.errors).toEqual({})
|
||||
})
|
||||
|
||||
it('vide le formulaire en mode creation (null)', () => {
|
||||
@@ -105,7 +109,7 @@ describe('useCategoryForm', () => {
|
||||
const ok = form.validate()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.errors.value.name).toBe('admin.categories.validation.nameRequired')
|
||||
expect(form.errors.name).toBe('admin.categories.validation.nameRequired')
|
||||
})
|
||||
|
||||
it('signale erreur si name est whitespace-only (trim → vide)', () => {
|
||||
@@ -116,7 +120,7 @@ describe('useCategoryForm', () => {
|
||||
const ok = form.validate()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.errors.value.name).toBe('admin.categories.validation.nameRequired')
|
||||
expect(form.errors.name).toBe('admin.categories.validation.nameRequired')
|
||||
})
|
||||
|
||||
it('signale erreur si name fait 1 caractere (< 2, RG-1.04)', () => {
|
||||
@@ -127,7 +131,7 @@ describe('useCategoryForm', () => {
|
||||
const ok = form.validate()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.errors.value.name).toBe('admin.categories.validation.nameLength')
|
||||
expect(form.errors.name).toBe('admin.categories.validation.nameLength')
|
||||
})
|
||||
|
||||
it('signale erreur si name fait 121 caracteres (> 120, RG-1.04)', () => {
|
||||
@@ -138,7 +142,7 @@ describe('useCategoryForm', () => {
|
||||
const ok = form.validate()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.errors.value.name).toBe('admin.categories.validation.nameLength')
|
||||
expect(form.errors.name).toBe('admin.categories.validation.nameLength')
|
||||
})
|
||||
|
||||
it('signale erreur si categoryTypeId est null (RG-1.05)', () => {
|
||||
@@ -149,7 +153,7 @@ describe('useCategoryForm', () => {
|
||||
const ok = form.validate()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.errors.value.categoryType).toBe('admin.categories.validation.typeRequired')
|
||||
expect(form.errors.categoryType).toBe('admin.categories.validation.typeRequired')
|
||||
})
|
||||
|
||||
it('passe quand name et categoryType sont valides', () => {
|
||||
@@ -160,19 +164,22 @@ describe('useCategoryForm', () => {
|
||||
const ok = form.validate()
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(form.errors.value).toEqual({ name: '', categoryType: '', _global: '' })
|
||||
expect(form.errors).toEqual({})
|
||||
})
|
||||
|
||||
it('reinitialise les erreurs avant chaque validation', () => {
|
||||
const form = useCategoryForm()
|
||||
// Erreur prealable.
|
||||
form.errors.value._global = 'erreur ancienne'
|
||||
form.name.value = 'Vis'
|
||||
// Erreur prealable : une validation en echec peuple errors.name.
|
||||
form.name.value = ''
|
||||
form.categoryTypeId.value = 1
|
||||
form.validate()
|
||||
expect(form.errors.name).toBeTruthy()
|
||||
|
||||
// Seconde validation avec des valeurs valides : errors repart vide.
|
||||
form.name.value = 'Vis'
|
||||
form.validate()
|
||||
|
||||
expect(form.errors.value._global).toBe('')
|
||||
expect(form.errors).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -231,8 +238,8 @@ describe('useCategoryForm', () => {
|
||||
expect(result).toBeNull()
|
||||
// La cle est interpolee avec le nom soumis : on retrouve "Vis" dans
|
||||
// les params i18n (stub serialise les params).
|
||||
expect(form.errors.value.name).toContain('admin.categories.toast.duplicate')
|
||||
expect(form.errors.value.name).toContain('"name":"Vis"')
|
||||
expect(form.errors.name).toContain('admin.categories.toast.duplicate')
|
||||
expect(form.errors.name).toContain('"name":"Vis"')
|
||||
expect(mockToastError).toHaveBeenCalledTimes(1)
|
||||
const toastArg = mockToastError.mock.calls[0]?.[0] as { message: string }
|
||||
expect(toastArg.message).toContain('Vis')
|
||||
@@ -256,7 +263,7 @@ describe('useCategoryForm', () => {
|
||||
const result = await form.submitCreate()
|
||||
|
||||
expect(result).toBeNull()
|
||||
expect(form.errors.value.name).toBe('name should not be blank.')
|
||||
expect(form.errors.name).toBe('name should not be blank.')
|
||||
// Pas de toast quand on a mappe les violations : l erreur est
|
||||
// affichee inline sous le champ concerne.
|
||||
expect(mockToastError).not.toHaveBeenCalled()
|
||||
@@ -279,10 +286,10 @@ describe('useCategoryForm', () => {
|
||||
|
||||
await form.submitCreate()
|
||||
|
||||
expect(form.errors.value.categoryType).toBe('Type invalide.')
|
||||
expect(form.errors.categoryType).toBe('Type invalide.')
|
||||
})
|
||||
|
||||
it('fallback en erreur globale + toast si le status n est ni 409 ni 422', async () => {
|
||||
it('fallback en toast generique si le status n est ni 409 ni 422', async () => {
|
||||
mockPost.mockRejectedValueOnce({
|
||||
response: { status: 500, _data: { 'hydra:description': 'Boom server' } },
|
||||
})
|
||||
@@ -292,7 +299,8 @@ describe('useCategoryForm', () => {
|
||||
|
||||
await form.submitCreate()
|
||||
|
||||
expect(form.errors.value._global).toBe('Boom server')
|
||||
// Pas d'erreur inline par champ : l'erreur transverse part en toast.
|
||||
expect(form.errors).toEqual({})
|
||||
expect(mockToastError).toHaveBeenCalledWith({
|
||||
title: 'Erreur',
|
||||
message: 'Boom server',
|
||||
@@ -386,8 +394,8 @@ describe('useCategoryForm', () => {
|
||||
const result = await form.submitUpdate(42)
|
||||
|
||||
expect(result).toBeNull()
|
||||
expect(form.errors.value.name).toContain('admin.categories.toast.duplicate')
|
||||
expect(form.errors.value.name).toContain('"name":"Doublon"')
|
||||
expect(form.errors.name).toContain('admin.categories.toast.duplicate')
|
||||
expect(form.errors.name).toContain('"name":"Doublon"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -415,7 +423,6 @@ describe('useCategoryForm', () => {
|
||||
const ok = await form.submitDelete(42)
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(form.errors.value._global).toBe('down')
|
||||
expect(mockToastError).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -424,15 +431,15 @@ describe('useCategoryForm', () => {
|
||||
it('vide le formulaire et les erreurs', () => {
|
||||
const form = useCategoryForm()
|
||||
form.loadFrom(CAT)
|
||||
form.name.value = 'edit'
|
||||
form.errors.value._global = 'erreur'
|
||||
form.name.value = ''
|
||||
form.validate() // peuple errors.name
|
||||
form.submitting.value = true
|
||||
|
||||
form.reset()
|
||||
|
||||
expect(form.name.value).toBe('')
|
||||
expect(form.categoryTypeId.value).toBeNull()
|
||||
expect(form.errors.value).toEqual({ name: '', categoryType: '', _global: '' })
|
||||
expect(form.errors).toEqual({})
|
||||
expect(form.submitting.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user