diff --git a/apps/web/cypress/e2e/recipe-sources.feature b/apps/web/cypress/e2e/recipe-sources.feature index 0225ac3..fc03862 100644 --- a/apps/web/cypress/e2e/recipe-sources.feature +++ b/apps/web/cypress/e2e/recipe-sources.feature @@ -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 diff --git a/apps/web/cypress/e2e/recipe-sources.ts b/apps/web/cypress/e2e/recipe-sources.ts index fea1b6d..54af571 100644 --- a/apps/web/cypress/e2e/recipe-sources.ts +++ b/apps/web/cypress/e2e/recipe-sources.ts @@ -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(); + 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