test(recipes): fiabilise le scénario 3 pages (scroll réel + log de curseurs)
All checks were successful
CI / lint (push) Successful in 2m7s
CI / build (push) Successful in 2m12s
CI / intent-service-test (push) Successful in 4m21s
CI / e2e (push) Successful in 6m11s
CI / test (push) Successful in 14m4s

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>
This commit is contained in:
Nicolas 2026-09-05 20:45:58 +02:00
parent c842fbb348
commit 1cd886cb8a
2 changed files with 33 additions and 15 deletions

View file

@ -64,11 +64,9 @@ Feature: Browsing external recipe sources
When I visit "/recettes"
And I click the button "TheMealDB"
Then I should see the source item "Page 1 Item 1"
When I scroll the source list to the bottom
Then I should see the source item "Page 2 Item 1"
When I scroll the source list to the bottom
Then I should see the source item "Page 3 Item 1"
And no browse cursor should have been requested more than once
When I scroll the source list to the bottom 5 times
Then the third page's items should have loaded
And no browse cursor should have been requested more than twice
Scenario: Offers a retry when loading the next page fails
Given the recipe catalog contains nothing

View file

@ -174,18 +174,38 @@ Given("browsing TheMealDB returns three pages, each addressed by its own cursor"
});
});
When("I scroll the source list to the bottom", () => {
cy.get(".source-items-column .recipe-table-wrap").scrollTo("bottom", {
ensureScrollable: false,
});
// IntersectionObserver callbacks are delivered on their own task, and the
// next page's fetch + merge then needs a render — give both a beat.
cy.wait(400);
When("I scroll the source list to the bottom {int} times", (times: number) => {
for (let attempt = 0; attempt < times; attempt += 1) {
cy.get(".source-items-column .recipe-table-wrap").scrollTo("bottom", {
ensureScrollable: false,
});
// IntersectionObserver callbacks land on their own task, and the next
// page's fetch + merge then needs a render — give both a beat before
// the next scroll (which needs fresh content below it to have anything
// new to reveal).
cy.wait(350);
}
});
Then("no browse cursor should have been requested more than once", () => {
const seen = [...threePageCursorsRequested];
expect(seen.length, `cursors requested: ${JSON.stringify(seen)}`).to.equal(new Set(seen).size);
Then("the third page's items should have loaded", () => {
// Page 3's marker sits ~30 rows down inside the list's own overflow
// container — scroll it in before asserting, like `common.steps.ts`'s
// ingredient-row check does. With the stale-closure loop it never loads
// at all: `handleLoadMore` keeps re-requesting cursor-2.
cy.contains(".recipe-table__name", "Page 3 Item 1").scrollIntoView().should("be.visible");
});
Then("no browse cursor should have been requested more than twice", () => {
const counts = new Map<string, number>();
for (const cursor of threePageCursorsRequested) {
counts.set(cursor, (counts.get(cursor) ?? 0) + 1);
}
// React StrictMode double-invokes effects in the e2e dev server (see
// recipes.cy.ts), so the panel's initial fetch + one-page-ahead prefetch
// can each fire twice — hence "twice", not "once". The issue #38 loop
// blows well past that: every scroll re-requests the same stale cursor.
const offenders = [...counts.entries()].filter(([, n]) => n > 2);
expect(offenders, `cursor request counts: ${JSON.stringify([...counts])}`).to.have.length(0);
});
// The panel prefetches the next page as soon as page 1 is on screen (before