refactor(transport) : onglet Qualimat en MalioDataTable paginé, recherche branchée sur le nom (ERP-166)
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { ref } from 'vue'
|
||||
import { debounce } from '~/shared/utils/debounce'
|
||||
import type { HydraCollection } from '~/shared/utils/api'
|
||||
import { usePaginatedList } from '~/shared/composables/usePaginatedList'
|
||||
|
||||
/**
|
||||
* Ligne du référentiel QUALIMAT renvoyée par la saisie assistée (groupe
|
||||
@@ -20,57 +18,23 @@ export interface QualimatCarrierRow {
|
||||
status: string | null
|
||||
}
|
||||
|
||||
/** Délai de debounce de la recherche (ms) — une requête après la dernière frappe. */
|
||||
const SEARCH_DEBOUNCE_MS = 300
|
||||
/** Filtre de la recherche QUALIMAT (branché sur le nom du transporteur). */
|
||||
export interface QualimatSearchFilters {
|
||||
search?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Saisie assistée QUALIMAT (M4 Transport, ERP-166 — RG-4.01 / spec-back § 4.7).
|
||||
*
|
||||
* `GET /api/qualimat_carriers?search=` : référentiel en LECTURE SEULE, lignes
|
||||
* actives uniquement (filtré côté serveur), recherche fuzzy nom + siret. Alimente
|
||||
* le tableau de sélection de l'onglet Qualimat ; la ligne choisie est copiée dans
|
||||
* le formulaire principal (cf. `useCarrierForm.applyQualimatSelection`).
|
||||
* actives uniquement (filtré côté serveur), recherche fuzzy nom + siret. Simple
|
||||
* enveloppe de `usePaginatedList` (règle frontend : toute GetCollection passe par
|
||||
* ce composable — pagination Hydra, état 100 % local) consommée par le
|
||||
* `MalioDataTable` de l'onglet Qualimat. Le filtre `search` est piloté par le nom
|
||||
* saisi dans le formulaire principal (pas de champ de recherche dédié).
|
||||
*
|
||||
* Volontairement PAR INSTANCE (état local à l'écran d'ajout). `search()` est
|
||||
* debouncé (anti-spam réseau) ; `fetchNow()` expose l'appel immédiat (montage /
|
||||
* tests).
|
||||
* Volontairement PAR INSTANCE (état local à l'écran d'ajout).
|
||||
*/
|
||||
export function useQualimatSearch() {
|
||||
const api = useApi()
|
||||
|
||||
const results = ref<QualimatCarrierRow[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
/** Lance immédiatement la recherche (sans debounce). */
|
||||
async function fetchNow(term: string): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.get<HydraCollection<QualimatCarrierRow>>(
|
||||
'/qualimat_carriers',
|
||||
{ search: term.trim() },
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
results.value = data.member ?? []
|
||||
}
|
||||
catch {
|
||||
// Échec réseau / 403 : on vide les résultats, pas de toast (la recherche
|
||||
// assistée est non bloquante — l'utilisateur peut saisir manuellement).
|
||||
results.value = []
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Recherche debouncée branchée sur le champ de recherche de l'onglet Qualimat.
|
||||
const search = debounce((term: string) => {
|
||||
void fetchNow(term)
|
||||
}, SEARCH_DEBOUNCE_MS)
|
||||
|
||||
return {
|
||||
results,
|
||||
loading,
|
||||
search,
|
||||
fetchNow,
|
||||
}
|
||||
return usePaginatedList<QualimatCarrierRow, QualimatSearchFilters>({ url: '/qualimat_carriers' })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user