Merge branch 'develop' into feature/ERP-116-country-referentiel
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m10s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m14s

This commit is contained in:
2026-06-11 07:38:35 +00:00
43 changed files with 6121 additions and 602 deletions
@@ -0,0 +1,95 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Mocks des composables auto-importes par Nuxt (indisponibles sous happy-dom).
const mockGet = vi.hoisted(() => vi.fn())
const mockPatch = vi.hoisted(() => vi.fn())
vi.stubGlobal('useApi', () => ({
get: mockGet,
post: vi.fn(),
put: vi.fn(),
patch: mockPatch,
delete: vi.fn(),
}))
const { useSupplier } = await import('../useSupplier')
const SAMPLE = { '@id': '/api/suppliers/85', id: 85, companyName: 'DOD59393F 862875', isArchived: false }
describe('useSupplier', () => {
beforeEach(() => {
mockGet.mockReset()
mockPatch.mockReset()
mockGet.mockResolvedValue(SAMPLE)
mockPatch.mockResolvedValue({ ...SAMPLE, isArchived: true })
})
it('charge le detail via GET /suppliers/{id} en Hydra, sans toast', async () => {
const { supplier, load } = useSupplier(85)
await load()
expect(mockGet).toHaveBeenCalledWith(
'/suppliers/85',
{},
expect.objectContaining({
headers: { Accept: 'application/ld+json' },
toast: false,
}),
)
expect(supplier.value).toEqual(SAMPLE)
})
it('bascule loading pendant le chargement et le retombe a false', async () => {
const { loading, load } = useSupplier(85)
const promise = load()
expect(loading.value).toBe(true)
await promise
expect(loading.value).toBe(false)
})
it('marque error et laisse supplier null si le GET echoue (404...)', async () => {
mockGet.mockRejectedValueOnce(new Error('not found'))
const { supplier, error, load } = useSupplier(99)
await load()
expect(error.value).toBe(true)
expect(supplier.value).toBeNull()
})
it('archive() PATCHe { isArchived: true } sans toast puis RECHARGE le detail complet', async () => {
// 1er GET = chargement initial, 2e GET = rechargement post-archivage.
mockGet.mockResolvedValueOnce(SAMPLE)
mockGet.mockResolvedValueOnce({ ...SAMPLE, isArchived: true })
const { supplier, load, archive } = useSupplier(85)
await load()
await archive()
expect(mockPatch).toHaveBeenCalledWith(
'/suppliers/85',
{ isArchived: true },
expect.objectContaining({ toast: false }),
)
// Le detail est re-fetch (le PATCH ne renvoie pas l'embed complet).
expect(mockGet).toHaveBeenCalledTimes(2)
expect(supplier.value?.isArchived).toBe(true)
})
it('restore() PATCHe { isArchived: false } (payload isArchived SEUL)', async () => {
const { load, restore } = useSupplier(85)
await load()
await restore()
expect(mockPatch).toHaveBeenCalledWith(
'/suppliers/85',
{ isArchived: false },
expect.objectContaining({ toast: false }),
)
})
it('propage l\'erreur (ex: 403 sans permission archive, 409 conflit homonyme) au lieu de l\'avaler', async () => {
const forbidden = { response: { status: 403 } }
mockPatch.mockRejectedValueOnce(forbidden)
const { load, archive } = useSupplier(85)
await load()
await expect(archive()).rejects.toBe(forbidden)
})
})
@@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
// `useApi` est un auto-import Nuxt : on le stubbe globalement pour intercepter
// les appels de chargement des referentiels et controler les reponses Hydra.
const mockGet = vi.hoisted(() => vi.fn())
vi.stubGlobal('useApi', () => ({ get: mockGet }))
const { useSupplierReferentials } = await import('../useSupplierReferentials')
describe('useSupplierReferentials', () => {
beforeEach(() => {
mockGet.mockReset()
mockGet.mockResolvedValue({ member: [] })
})
it('charge les categories filtrees sur le type FOURNISSEUR (RG-2.10)', async () => {
await useSupplierReferentials().loadCommon()
expect(mockGet).toHaveBeenCalledWith(
'/categories',
expect.objectContaining({ pagination: 'false', typeCode: 'FOURNISSEUR' }),
expect.objectContaining({ toast: false }),
)
})
it('mappe les categories en options { value: IRI, label: name, code }', async () => {
mockGet.mockImplementation((url: string) => {
if (url === '/categories') {
return Promise.resolve({ member: [{ '@id': '/api/categories/9', code: 'NEGOCIANT', name: 'Négociant' }] })
}
return Promise.resolve({ member: [] })
})
const refs = useSupplierReferentials()
await refs.loadCommon()
expect(refs.categories.value).toEqual([{ value: '/api/categories/9', label: 'Négociant', code: 'NEGOCIANT' }])
})
it('ne charge ni distributeurs ni courtiers (absents du modele fournisseur)', async () => {
await useSupplierReferentials().loadCommon()
const urls = mockGet.mock.calls.map(c => c[0])
expect(urls).not.toContain('/clients')
expect(urls).toEqual(
expect.arrayContaining(['/categories', '/sites', '/tva_modes', '/payment_delays', '/payment_types', '/banks']),
)
})
it('reste resilient : un referentiel en echec n\'empeche pas les autres', async () => {
mockGet.mockImplementation((url: string) => {
if (url === '/categories') return Promise.reject(new Error('403'))
if (url === '/banks') return Promise.resolve({ member: [{ '@id': '/api/banks/1', code: 'SG', label: 'Société Générale' }] })
return Promise.resolve({ member: [] })
})
const refs = useSupplierReferentials()
await refs.loadCommon()
expect(refs.categories.value).toEqual([])
expect(refs.banks.value).toEqual([{ value: '/api/banks/1', label: 'Société Générale' }])
})
})
@@ -0,0 +1,85 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { HydraCollection } from '~/shared/utils/api'
import type { Supplier } from '../useSuppliersRepository'
// `useApi` est un auto-import Nuxt : on le stubbe globalement pour intercepter
// les appels declenches par usePaginatedList (que useSuppliersRepository enveloppe)
// et controler les reponses. Meme pattern que useClientsRepository.spec.ts.
const mockGet = vi.hoisted(() => vi.fn())
vi.stubGlobal('useApi', () => ({
get: mockGet,
post: vi.fn(),
put: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
}))
// Import APRES le stub pour que useApi soit bien resolu au top-level du module.
const { useSuppliersRepository } = await import('../useSuppliersRepository')
/** Envelope Hydra minimale (la liste reelle des membres importe peu ici). */
function makeHydra(total: number): HydraCollection<Supplier> {
return { totalItems: total, member: [] }
}
describe('useSuppliersRepository', () => {
beforeEach(() => {
mockGet.mockReset()
// 25 items → 3 pages a 10/page : permet de tester la navigation page 2.
mockGet.mockResolvedValue(makeHydra(25))
})
it('cible la ressource /suppliers en page 1 par defaut', async () => {
const repo = useSuppliersRepository()
await repo.fetch()
expect(mockGet).toHaveBeenLastCalledWith(
'/suppliers',
{ page: 1, itemsPerPage: 10 },
expect.objectContaining({ toast: false }),
)
})
it('pousse les filtres du drawer (categories multi, sites, archives inclus) et retombe en page 1', async () => {
const repo = useSuppliersRepository()
await repo.fetch()
await repo.goToPage(2)
expect(repo.currentPage.value).toBe(2)
await repo.setFilters(
{
search: 'acme',
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
'siteId[]': ['86', '17'],
includeArchived: true,
},
{ replace: true },
)
expect(repo.currentPage.value).toBe(1)
expect(mockGet).toHaveBeenLastCalledWith(
'/suppliers',
{
search: 'acme',
'categoryCode[]': ['NEGOCIANT', 'TRANSPORTEUR'],
'siteId[]': ['86', '17'],
includeArchived: true,
page: 1,
itemsPerPage: 10,
},
expect.objectContaining({ toast: false }),
)
})
it('repasse a une query propre apres reinitialisation des filtres', async () => {
const repo = useSuppliersRepository()
await repo.setFilters({ search: 'acme', includeArchived: true }, { replace: true })
await repo.setFilters({}, { replace: true })
expect(mockGet).toHaveBeenLastCalledWith(
'/suppliers',
{ page: 1, itemsPerPage: 10 },
expect.objectContaining({ toast: false }),
)
})
})