d0e9f48983
Auto Tag Develop / tag (push) Successful in 8s
Empilée sur ERP-140 (#102). ## Périmètre ERP-141 Écran `/providers/new` — création par onglets + formulaire principal (POST). - **Page** `modules/technique/pages/providers/new.vue` : en-tête + retour, formulaire principal (Nom, Catégorie, Site), barre d'onglets **Contact · Adresse · Comptabilité** (pas d'onglet Information ; Rapports/Échanges absents en création). Contenu des onglets = placeholders « À venir » (ERP-142→144). - **`useProviderForm()`** : POST principal (groupe `provider:write:main`, IRIs catégories/sites), pré-check front RG-3.03 (≥1 site) / RG-3.09 (≥1 catégorie), 409 doublon (RG-3.10) inline, 422 mapping par champ via `useFormErrors`, orchestration des onglets (verrouillage + bascule auto sur Contact au succès), `patchProvider` (PATCH partiel mode strict pour les onglets à venir). - **`useProviderReferentials()`** : catégories type PRESTATAIRE + sites (`?pagination=false`, Hydra). - i18n `technique.providers.form/tab/toast`. ## Conformité - `useApi()` uniquement, composants `Malio*`, aucun texte FR en dur, bouton « Valider » toujours actif + erreurs sous les champs (ERP-101). ## Vérifications - Vitest : 402/402 (dont 9 nouveaux tests `useProviderForm`). - ESLint : OK. - `nuxi typecheck` : 0 erreur sur les fichiers source du ticket. - Golden path navigateur : page rendue, catégories filtrées PRESTATAIRE, sélecteur site, onglets désactivés avant validation, erreurs inline RG-3.03/3.09. Reviewed-on: #103 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
193 lines
7.5 KiB
TypeScript
193 lines
7.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
/**
|
|
* Tests du workflow « Ajouter un prestataire » (M3 Technique, ERP-141).
|
|
*
|
|
* `useProviderForm` porte le formulaire principal (Nom + Categorie + Site) et
|
|
* l'orchestration des onglets de creation. On verifie ici le CONTRAT propre a la
|
|
* creation :
|
|
* - RG-3.03 (front) : au moins un site requis ; RG-3.09 : au moins une categorie
|
|
* -> POST bloque, erreurs inline, aucun appel reseau.
|
|
* - POST /providers (groupe provider:write:main) : payload IRIs + Accept ld+json
|
|
* + toast:false ; au succes, verrouillage + bascule sur l'onglet Contact +
|
|
* reaffichage du nom normalise.
|
|
* - 409 doublon (RG-3.10) -> erreur inline dediee sur companyName.
|
|
* - 422 -> mapping inline par champ (propertyPath).
|
|
* - Onglets : « Comptabilite » present uniquement avec accounting.view ;
|
|
* completeTab deverrouille/avance et signale le dernier onglet.
|
|
*/
|
|
|
|
const mockPost = vi.hoisted(() => vi.fn())
|
|
const mockPatch = vi.hoisted(() => vi.fn())
|
|
// Permission accounting.view pilotable par test (presence de l'onglet Comptabilite).
|
|
const permState = vi.hoisted(() => ({ accountingView: false }))
|
|
|
|
vi.stubGlobal('useApi', () => ({
|
|
get: vi.fn(),
|
|
post: mockPost,
|
|
put: vi.fn(),
|
|
patch: mockPatch,
|
|
delete: vi.fn(),
|
|
}))
|
|
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
|
vi.stubGlobal('useToast', () => ({
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
warning: vi.fn(),
|
|
info: vi.fn(),
|
|
}))
|
|
vi.stubGlobal('usePermissions', () => ({
|
|
can: (perm: string) => perm === 'technique.providers.accounting.view' ? permState.accountingView : true,
|
|
}))
|
|
|
|
const { useProviderForm, buildProviderCreateTabKeys } = await import('../useProviderForm')
|
|
|
|
const SITE_86 = '/api/sites/1'
|
|
const CAT_MAINT = '/api/categories/7'
|
|
|
|
describe('useProviderForm', () => {
|
|
beforeEach(() => {
|
|
mockPost.mockReset()
|
|
mockPatch.mockReset()
|
|
permState.accountingView = false
|
|
})
|
|
|
|
it('RG-3.03/RG-3.09 (front) : bloque le POST si aucun site / aucune categorie', async () => {
|
|
const form = useProviderForm()
|
|
form.main.companyName = 'Maintenance Pro'
|
|
|
|
const created = await form.submitMain()
|
|
|
|
expect(created).toBe(false)
|
|
expect(mockPost).not.toHaveBeenCalled()
|
|
expect(form.mainErrors.errors.sites).toBe('technique.providers.form.errors.siteRequired')
|
|
expect(form.mainErrors.errors.categories).toBe('technique.providers.form.errors.categoryRequired')
|
|
expect(form.mainLocked.value).toBe(false)
|
|
})
|
|
|
|
it('RG-3.03 (front) : un site present sans categorie n\'erre que sur categories', async () => {
|
|
const form = useProviderForm()
|
|
form.main.companyName = 'Maintenance Pro'
|
|
form.main.siteIris = [SITE_86]
|
|
|
|
await form.submitMain()
|
|
|
|
expect(mockPost).not.toHaveBeenCalled()
|
|
expect(form.mainErrors.errors.sites).toBeUndefined()
|
|
expect(form.mainErrors.errors.categories).toBe('technique.providers.form.errors.categoryRequired')
|
|
})
|
|
|
|
it('POST /providers avec IRIs + Accept ld+json, verrouille et bascule sur Contact', async () => {
|
|
mockPost.mockResolvedValueOnce({ id: 42, companyName: 'MAINTENANCE PRO' })
|
|
const form = useProviderForm()
|
|
form.main.companyName = 'Maintenance Pro'
|
|
form.main.categoryIris = [CAT_MAINT]
|
|
form.main.siteIris = [SITE_86]
|
|
|
|
const created = await form.submitMain()
|
|
|
|
expect(created).toBe(true)
|
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
|
const [url, body, opts] = mockPost.mock.calls[0] ?? []
|
|
expect(url).toBe('/providers')
|
|
expect(body).toEqual({
|
|
companyName: 'Maintenance Pro',
|
|
categories: [CAT_MAINT],
|
|
sites: [SITE_86],
|
|
})
|
|
expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
|
|
|
|
expect(form.providerId.value).toBe(42)
|
|
// RG-3.11 : reaffiche le nom normalise (UPPERCASE) renvoye par le serveur.
|
|
expect(form.main.companyName).toBe('MAINTENANCE PRO')
|
|
expect(form.mainLocked.value).toBe(true)
|
|
expect(form.activeTab.value).toBe('contact')
|
|
expect(form.unlockedIndex.value).toBe(0)
|
|
})
|
|
|
|
it('omet companyName vide du payload (laisse la 422 NotBlank back mordre)', async () => {
|
|
mockPost.mockResolvedValueOnce({ id: 1, companyName: null })
|
|
const form = useProviderForm()
|
|
form.main.companyName = ' '
|
|
form.main.categoryIris = [CAT_MAINT]
|
|
form.main.siteIris = [SITE_86]
|
|
|
|
await form.submitMain()
|
|
|
|
const body = (mockPost.mock.calls[0] ?? [])[1] as Record<string, unknown>
|
|
expect(body).not.toHaveProperty('companyName')
|
|
expect(body).toEqual({ categories: [CAT_MAINT], sites: [SITE_86] })
|
|
})
|
|
|
|
it('409 doublon (RG-3.10) : erreur inline dediee sur companyName, pas de verrouillage', async () => {
|
|
mockPost.mockRejectedValueOnce({ response: { status: 409 } })
|
|
const form = useProviderForm()
|
|
form.main.companyName = 'Doublon'
|
|
form.main.categoryIris = [CAT_MAINT]
|
|
form.main.siteIris = [SITE_86]
|
|
|
|
const created = await form.submitMain()
|
|
|
|
expect(created).toBe(false)
|
|
expect(form.mainErrors.errors.companyName).toBe('technique.providers.form.duplicateCompany')
|
|
expect(form.mainLocked.value).toBe(false)
|
|
})
|
|
|
|
it('422 : mappe les violations serveur inline par champ', async () => {
|
|
mockPost.mockRejectedValueOnce({
|
|
response: {
|
|
status: 422,
|
|
_data: { violations: [{ propertyPath: 'sites', message: 'Au moins un site est requis.' }] },
|
|
},
|
|
})
|
|
const form = useProviderForm()
|
|
form.main.companyName = 'X'
|
|
form.main.categoryIris = [CAT_MAINT]
|
|
form.main.siteIris = [SITE_86]
|
|
|
|
const created = await form.submitMain()
|
|
|
|
expect(created).toBe(false)
|
|
expect(form.mainErrors.errors.sites).toBe('Au moins un site est requis.')
|
|
})
|
|
|
|
it('onglet Comptabilite : absent sans accounting.view, present avec', () => {
|
|
expect(buildProviderCreateTabKeys(false)).toEqual(['contact', 'address'])
|
|
expect(buildProviderCreateTabKeys(true)).toEqual(['contact', 'address', 'accounting'])
|
|
|
|
permState.accountingView = true
|
|
const form = useProviderForm()
|
|
expect(form.tabKeys.value).toEqual(['contact', 'address', 'accounting'])
|
|
})
|
|
|
|
it('completeTab : deverrouille/avance, et signale le dernier onglet du flux', () => {
|
|
const form = useProviderForm()
|
|
|
|
// Contact -> Adresse (pas le dernier).
|
|
expect(form.completeTab('contact')).toBe(false)
|
|
expect(form.isValidated('contact')).toBe(true)
|
|
expect(form.activeTab.value).toBe('address')
|
|
expect(form.unlockedIndex.value).toBe(1)
|
|
|
|
// Adresse = dernier onglet remplissable (sans accounting.view) -> true.
|
|
expect(form.completeTab('address')).toBe(true)
|
|
expect(form.isValidated('address')).toBe(true)
|
|
})
|
|
|
|
it('patchProvider : PATCH /providers/{id} en mode strict, no-op avant creation', async () => {
|
|
const form = useProviderForm()
|
|
|
|
await form.patchProvider({ siren: '123456789' })
|
|
expect(mockPatch).not.toHaveBeenCalled()
|
|
|
|
mockPost.mockResolvedValueOnce({ id: 9, companyName: 'ACME' })
|
|
form.main.companyName = 'Acme'
|
|
form.main.categoryIris = [CAT_MAINT]
|
|
form.main.siteIris = [SITE_86]
|
|
await form.submitMain()
|
|
|
|
await form.patchProvider({ siren: '123456789' })
|
|
expect(mockPatch).toHaveBeenCalledWith('/providers/9', { siren: '123456789' }, { toast: false })
|
|
})
|
|
})
|