fix(recipes): stoppe le scroll infini des sources au lieu de boucler #46
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/source-scroll-loop"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Corrige l'issue #38 — deux volets.
1. Le bug de bouclage
L'
IntersectionObserverdeSourceItemTable.tsxa[hasMore]comme seule dépendance et ne se réabonne donc jamais pendant un scroll qui progresse normalement (hasMorerestetrue). Il capturait pour toujours la première closureonLoadMore(=handleLoadMore, redéclarée à chaque rendu, qui figebrowseState.nextCursorpar valeur). Chaque intersection suivante rejouait la même closure figée → même curseur périmé redemandé en boucle → merge sans déduplication → la liste s'allonge indéfiniment avec les mêmes éléments,nextCursorn'atteint jamais son vrainull, la sentinelle reste montée, l'observer continue de se déclencher.Fix :
SourceItemTable.tsx— pattern « latest ref » :onLoadMoreest lu via uneuseRefréassignée à chaque rendu. L'observer appelle toujours la closure la plus fraîche, sans réabonnement ([hasMore]inchangé — l'intention dubiome-ignore, désormais inutile, est conservée).RecipeSourcesPanel.tsx— le merge dehandleLoadMoredéduplique parexternalIdet traite « cette page n'a rien ajouté de nouveau » comme une fin de liste locale, même si la source renvoie encore unnextCursornon nul. Garde-fou complémentaire (heuristique de fin imparfaite d'un adaptateur).2. Maximiser les recettes en recherche vide (TheMealDB)
theMealDbAdapter.list()tapait/search.php?s=même sansquery— TheMealDB borne ça à un échantillon fixe (~25 recettes),nextCursorcodé en dur ànull. Le scroll infini n'avait donc rien à charger.Une
queryvide/absente déroule maintenant la liste des catégories :/list.php?c=listune fois, puis/filter.php?c={catégorie}une catégorie à la fois, lecursorétant l'index de catégorie (une catégorie = une « page »). La liste des catégories est re-récupérée à chaque pas plutôt que mise en cache — une petite requête, l'adaptateur reste sans état, et c'est de toute façon cadencé au rythme du scroll. 750g / Marmiton / Manger Bouger : aucun changement (déjà corrects).Tests
apps/web/cypress/e2e/recipe-sources.feature+.ts— nouveau scénario à 3 pages réelles avec curseurs distincts effectivement vérifiés ; assertion « aucun curseur redemandé plus d'une fois » (échoue avec l'ancien code, où le curseur périmé est redemandé en boucle).apps/api/test/recipe-sources/the-meal-db.test.ts— marche par catégories : première page + curseur suivant, suivi du curseur, arrêt (nextCursor: null) sur la dernière catégorie, page finale vide au-delà, requête blanche traitée comme requête vide, saut d'un repas sans titre.specs/backend-architecture.md— paragraphe TheMealDB mis à jour.🤖 Generated with Claude Code
Le scénario ajouté supposait que l'IntersectionObserver se redéclenchait tout seul avec une liste courte — faux : il ne se déclenche qu'à une *transition* d'intersection. Les pages font maintenant 15 éléments (la liste déborde son conteneur, la sentinelle démarre sous la ligne de flottaison) et chaque page suivante est demandée via un `scrollTo("bottom")` explicite, qui produit une vraie transition. Avec l'ancien code bugué, la sentinelle stale rappelle « cursor-2 » au 2e scroll : « Page 3 Item 1 » n'apparaît jamais et un curseur est redemandé — les deux assertions échouent. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>Le scénario échouait en CI sur deux points de fragilité, pas sur le fond : 1. `IntersectionObserver` ne se redéclenche qu'à une *transition* d'intersection — avec des pages courtes la sentinelle restait visible en continu et seule la 2e page se chargeait. Les pages font maintenant 15 éléments (la liste déborde son conteneur) et chaque page suivante est tirée par un `scrollTo("bottom")` explicite, répété 5 fois. 2. Une fois la liste débordante, `should("be.visible")` sur un élément sorti du conteneur `overflow:auto` échoue (même classe de bug que #1) — l'assertion "3e page chargée" fait maintenant `.scrollIntoView()` avant. L'assertion anti-boucle porte sur le journal des curseurs demandés (tolérance 2× pour le double-effet React StrictMode du serveur e2e), pas sur un `Set.size` strict. Vérifié localement : 7/7 avec le correctif, "Page 3 Item 1" jamais chargée sans lui. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>