fix(recipes): stoppe le scroll infini des sources au lieu de boucler #46
2 changed files with 44 additions and 27 deletions
|
|
@ -63,9 +63,11 @@ Feature: Browsing external recipe sources
|
|||
And browsing TheMealDB returns three pages, each addressed by its own cursor
|
||||
When I visit "/recettes"
|
||||
And I click the button "TheMealDB"
|
||||
Then I should see the source item "Page One Dish"
|
||||
And I should see the source item "Page Two Dish"
|
||||
And I should see the source item "Page Three Dish"
|
||||
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
|
||||
|
||||
Scenario: Offers a retry when loading the next page fails
|
||||
|
|
|
|||
|
|
@ -133,28 +133,39 @@ Given("browsing TheMealDB returns two pages of items", () => {
|
|||
});
|
||||
|
||||
/**
|
||||
* Three real pages, each addressed by a distinct `cursor` value the mock
|
||||
* actually parses (not just "does the URL contain `cursor=`"). Records
|
||||
* every cursor it serves so the scenario can assert none was requested
|
||||
* twice — the tell-tale of the issue #38 loop, where a stale closure
|
||||
* re-requests the same page forever instead of advancing.
|
||||
* Three full pages, each addressed by a distinct `cursor` value the mock
|
||||
* actually parses (not just "does the URL contain `cursor=`"). Pages are
|
||||
* deliberately large enough to overflow the list's own scroll container so
|
||||
* the sentinel starts below the fold — each `scrollTo("bottom")` then
|
||||
* produces a real IntersectionObserver transition, the only thing that
|
||||
* makes the observer fire again. Records every cursor it serves so the
|
||||
* scenario can assert none was requested twice — the tell-tale of the
|
||||
* issue #38 loop, where a stale closure re-requests the same page forever
|
||||
* instead of advancing.
|
||||
*/
|
||||
const THREE_PAGE_SIZE = 15;
|
||||
const threePageCursorsRequested: string[] = [];
|
||||
|
||||
function threePagePage(pageNo: number, nextCursor: string | null) {
|
||||
return {
|
||||
items: Array.from({ length: THREE_PAGE_SIZE }, (_, index) => ({
|
||||
externalId: `p${pageNo}-${index}`,
|
||||
title: `Page ${pageNo} Item ${index + 1}`,
|
||||
picture: null,
|
||||
url: `https://www.themealdb.com/meal/p${pageNo}-${index}`,
|
||||
alreadyImported: false,
|
||||
recipeId: null,
|
||||
})),
|
||||
nextCursor,
|
||||
};
|
||||
}
|
||||
|
||||
Given("browsing TheMealDB returns three pages, each addressed by its own cursor", () => {
|
||||
threePageCursorsRequested.length = 0;
|
||||
const item = (externalId: string, title: string) => ({
|
||||
externalId,
|
||||
title,
|
||||
picture: null,
|
||||
url: `https://www.themealdb.com/meal/${externalId}`,
|
||||
alreadyImported: false,
|
||||
recipeId: null,
|
||||
});
|
||||
const pages: Record<string, { items: unknown[]; nextCursor: string | null }> = {
|
||||
"": { items: [item("p1", "Page One Dish")], nextCursor: "cursor-2" },
|
||||
"cursor-2": { items: [item("p2", "Page Two Dish")], nextCursor: "cursor-3" },
|
||||
"cursor-3": { items: [item("p3", "Page Three Dish")], nextCursor: null },
|
||||
const pages: Record<string, ReturnType<typeof threePagePage>> = {
|
||||
"": threePagePage(1, "cursor-2"),
|
||||
"cursor-2": threePagePage(2, "cursor-3"),
|
||||
"cursor-3": threePagePage(3, null),
|
||||
};
|
||||
cy.intercept("GET", "**/sources/theMealDb/browse*", (req) => {
|
||||
const cursor = new URL(req.url).searchParams.get("cursor") ?? "";
|
||||
|
|
@ -163,14 +174,18 @@ 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);
|
||||
});
|
||||
|
||||
Then("no browse cursor should have been requested more than once", () => {
|
||||
// Let the panel settle (auto-scroll + one-page-ahead prefetch) before
|
||||
// asserting. A looping IntersectionObserver would re-hit "" or "cursor-2"
|
||||
// here; the latest-ref fix keeps each cursor to a single request.
|
||||
cy.wait(600).then(() => {
|
||||
const seen = [...threePageCursorsRequested];
|
||||
expect(seen.length, `cursors requested: ${JSON.stringify(seen)}`).to.equal(new Set(seen).size);
|
||||
});
|
||||
});
|
||||
|
||||
// The panel prefetches the next page as soon as page 1 is on screen (before
|
||||
|
|
|
|||
Loading…
Reference in a new issue