refactor(transport) : onglet Qualimat en MalioDataTable paginé, recherche branchée sur le nom (ERP-166)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m1s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m44s

This commit is contained in:
2026-06-16 18:02:16 +02:00
parent d6d2144cc1
commit 388d39a379
4 changed files with 144 additions and 150 deletions
@@ -1,55 +1,62 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useQualimatSearch, type QualimatCarrierRow } from '../useQualimatSearch'
const mockApiGet = vi.hoisted(() => vi.fn())
vi.stubGlobal('useApi', () => ({ get: mockApiGet }))
/**
* Tests de la saisie assistée QUALIMAT (M4 Transport, ERP-166 — RG-4.01).
*
* `useQualimatSearch` interroge `GET /api/qualimat_carriers?search=`. On vérifie le
* CONTRAT (pas le timing du debounce, couvert par `debounce.test.ts`) via `fetchNow` :
* - ressource ciblée + paramètre `search` (trimé) + header `Accept: application/ld+json` ;
* - consommation de l'enveloppe Hydra (`member`) ;
* - échec réseau → résultats vidés, pas de throw (recherche non bloquante).
* `useQualimatSearch` est une fine enveloppe de `usePaginatedList<QualimatCarrierRow>`
* sur `/qualimat_carriers`. La pagination générique est couverte par
* `usePaginatedList.test.ts` ; on vérifie ici le CONTRAT propre à la recherche :
* - ressource ciblée `/qualimat_carriers` + enveloppe Hydra + `Accept: application/ld+json` ;
* - le filtre `search` (branché sur le nom du transporteur) est transmis et
* retombe en page 1.
*/
const mockGet = vi.hoisted(() => vi.fn())
vi.stubGlobal('useApi', () => ({
get: mockGet,
post: vi.fn(),
put: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
}))
const { useQualimatSearch } = await import('../useQualimatSearch')
describe('useQualimatSearch', () => {
beforeEach(() => {
mockGet.mockReset()
mockApiGet.mockReset()
})
it('fetchNow cible /qualimat_carriers (search trimé, ld+json) et consomme member', async () => {
mockGet.mockResolvedValueOnce({
member: [{ '@id': '/api/qualimat_carriers/1', id: '1', name: 'ACME', validityDate: '2027-01-01' }],
})
const q = useQualimatSearch()
const PAGE: QualimatCarrierRow[] = [
{
'@id': '/api/qualimat_carriers/1',
id: '1',
name: 'TRANSPORTS ACME',
siret: '12345678900012',
address: '1 rue du Port',
postalCode: '86000',
city: 'Poitiers',
validityDate: '2027-01-15',
status: 'VALIDE',
},
]
await q.fetchNow(' acme ')
it('cible /qualimat_carriers, consomme l\'enveloppe Hydra et envoie l\'Accept ld+json', async () => {
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
const repo = useQualimatSearch()
expect(mockGet).toHaveBeenCalledWith(
'/qualimat_carriers',
{ search: 'acme' },
{ headers: { Accept: 'application/ld+json' }, toast: false },
)
expect(q.results.value).toHaveLength(1)
expect(q.results.value[0]?.name).toBe('ACME')
expect(q.loading.value).toBe(false)
await repo.fetch()
const [url, query, opts] = mockApiGet.mock.calls[0]
expect(url).toBe('/qualimat_carriers')
expect(query).toMatchObject({ page: 1, itemsPerPage: 10 })
expect(opts).toMatchObject({ toast: false, headers: { Accept: 'application/ld+json' } })
expect(repo.items.value).toEqual(PAGE)
expect(repo.totalItems.value).toBe(1)
})
it('échec réseau : résultats vidés, pas de throw', async () => {
mockGet.mockRejectedValueOnce(new Error('network'))
const q = useQualimatSearch()
it('transmet le filtre `search` (nom du transporteur) et retombe en page 1', async () => {
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
const repo = useQualimatSearch()
await repo.fetch()
await expect(q.fetchNow('x')).resolves.toBeUndefined()
expect(q.results.value).toEqual([])
expect(q.loading.value).toBe(false)
mockApiGet.mockResolvedValueOnce({ member: PAGE, totalItems: 1 })
await repo.setFilters({ search: 'acme' })
expect(repo.currentPage.value).toBe(1)
const query = mockApiGet.mock.calls.at(-1)?.[1] as Record<string, unknown>
expect(query.search).toBe('acme')
})
})