fix(front) : usePaginatedList — garde anti-reponse periemee, expose error, durcit buildQuery
- ajoute un jeton de sequence dans fetch() : ignore les reponses arrivees apres une requete plus recente (race page/tri/filtres sur reseau lent) - expose une ref error pour distinguer liste vide d'un echec reseau/403 (isEmpty ne suffit pas a lever l'ambiguite) - buildQuery : cles reservees (page/itemsPerPage/order) assignees en dernier pour qu'un filtre homonyme ne les ecrase pas - corrige le commentaire trompeur sur extraQuery (snapshot statique) - nettoie le test hors-borne qui testait en realite le cas standard + ajoute tests error/reset-error/race
This commit is contained in:
@@ -231,8 +231,11 @@ describe('usePaginatedList', () => {
|
||||
expect(q['order[name]']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('page hors borne apres filtre retombe sur la derniere page valide', async () => {
|
||||
// 1er fetch : page 1 sur une grosse liste
|
||||
it('setFilters retombe en page 1 (cas standard, pas le hors-borne)', async () => {
|
||||
// setFilters remet toujours page=1 avant de refetcher : ce n'est
|
||||
// donc PAS le chemin de retry hors-borne (couvert par le test
|
||||
// suivant via un refetch a page constante). On verifie juste le
|
||||
// reset de page ici.
|
||||
mockResponse([], 50) // 5 pages
|
||||
const list = usePaginatedList({ url: '/users' })
|
||||
await list.fetch()
|
||||
@@ -240,21 +243,11 @@ describe('usePaginatedList', () => {
|
||||
await list.goToPage(5)
|
||||
expect(list.currentPage.value).toBe(5)
|
||||
|
||||
// Application d'un filtre : la nouvelle reponse a 12 items
|
||||
// (donc 2 pages) mais on demande page=5 → l'API renvoie member=[]
|
||||
// et le composable doit refetcher sur page=2.
|
||||
mockApiGet.mockReset()
|
||||
mockApiGet
|
||||
// 1er appel : page=5 alors qu'il ne reste que 2 pages
|
||||
.mockResolvedValueOnce({ member: [], totalItems: 12 })
|
||||
// 2eme appel : refetch automatique sur page=2
|
||||
.mockResolvedValueOnce({ member: [{ id: 1 }, { id: 2 }], totalItems: 12 })
|
||||
|
||||
mockResponse([{ id: 1 }, { id: 2 }], 12)
|
||||
await list.setFilters({ active: true } as never)
|
||||
|
||||
// setFilters reset page a 1 → c'est le cas standard, pas le hors borne.
|
||||
// Pour declencher le cas hors borne, on doit forcer la page > totalPages.
|
||||
expect(list.currentPage.value).toBe(1)
|
||||
expect(mockApiGet.mock.calls.at(-1)?.[1]).toMatchObject({ page: 1 })
|
||||
})
|
||||
|
||||
it('declenche le retry sur derniere page si currentPage > totalPages apres fetch', async () => {
|
||||
@@ -332,7 +325,8 @@ describe('usePaginatedList', () => {
|
||||
})
|
||||
|
||||
it('swallow l\'erreur reseau : items vides, loading false, fetch ne reject pas', async () => {
|
||||
mockApiGet.mockRejectedValueOnce(new Error('boom'))
|
||||
const boom = new Error('boom')
|
||||
mockApiGet.mockRejectedValueOnce(boom)
|
||||
const list = usePaginatedList({ url: '/users' })
|
||||
|
||||
await expect(list.fetch()).resolves.toBeUndefined()
|
||||
@@ -341,6 +335,47 @@ describe('usePaginatedList', () => {
|
||||
expect(list.loading.value).toBe(false)
|
||||
// L'erreur est consideree comme un fetch consume -> isEmpty=true.
|
||||
expect(list.isEmpty.value).toBe(true)
|
||||
// ... mais `error` est expose pour distinguer « vide » d'« echec ».
|
||||
expect(list.error.value).toBe(boom)
|
||||
})
|
||||
|
||||
it('error est remis a null des qu\'un fetch ulterieur reussit', async () => {
|
||||
mockApiGet.mockRejectedValueOnce(new Error('boom'))
|
||||
const list = usePaginatedList({ url: '/users' })
|
||||
await list.fetch()
|
||||
expect(list.error.value).toBeInstanceOf(Error)
|
||||
|
||||
mockResponse([{ id: 1 }], 1)
|
||||
await list.fetch()
|
||||
expect(list.error.value).toBeNull()
|
||||
expect(list.items.value).toEqual([{ id: 1 }])
|
||||
})
|
||||
|
||||
it('ignore une reponse periemee : la derniere requete *demandee* gagne', async () => {
|
||||
// Deux fetch concurrents : le 1er resout APRES le 2eme. Sans garde
|
||||
// de sequence, la reponse arrivee en dernier (token 1) ecraserait
|
||||
// les donnees plus fraiches du token 2. Avec la garde, token 2 fait
|
||||
// foi quel que soit l'ordre d'arrivee reseau.
|
||||
const list = usePaginatedList<{ id: number }>({ url: '/users' })
|
||||
|
||||
let resolveSlow!: (v: unknown) => void
|
||||
const slow = new Promise((r) => { resolveSlow = r })
|
||||
// 1er appel : reponse lente (en vol).
|
||||
mockApiGet.mockReturnValueOnce(slow)
|
||||
// 2eme appel : reponse immediate avec des donnees plus fraiches.
|
||||
mockApiGet.mockResolvedValueOnce({ member: [{ id: 2 }], totalItems: 30 })
|
||||
|
||||
const p1 = list.fetch() // token 1, en vol
|
||||
const p2 = list.fetch() // token 2, resout tout de suite
|
||||
await p2
|
||||
expect(list.items.value).toEqual([{ id: 2 }])
|
||||
|
||||
// La reponse lente du token 1 arrive enfin : elle doit etre ignoree.
|
||||
resolveSlow({ member: [{ id: 1 }], totalItems: 30 })
|
||||
await p1
|
||||
expect(list.items.value).toEqual([{ id: 2 }])
|
||||
// Le spinner reste eteint (la requete recente l'avait deja coupe).
|
||||
expect(list.loading.value).toBe(false)
|
||||
})
|
||||
|
||||
it('extraQuery est injecte a chaque fetch (ex : includeDeleted)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user