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
|
And browsing TheMealDB returns three pages, each addressed by its own cursor
|
||||||
When I visit "/recettes"
|
When I visit "/recettes"
|
||||||
And I click the button "TheMealDB"
|
And I click the button "TheMealDB"
|
||||||
Then I should see the source item "Page One Dish"
|
Then I should see the source item "Page 1 Item 1"
|
||||||
And I should see the source item "Page Two Dish"
|
When I scroll the source list to the bottom
|
||||||
And I should see the source item "Page Three Dish"
|
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
|
And no browse cursor should have been requested more than once
|
||||||
|
|
||||||
Scenario: Offers a retry when loading the next page fails
|
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
|
* Three full pages, each addressed by a distinct `cursor` value the mock
|
||||||
* actually parses (not just "does the URL contain `cursor=`"). Records
|
* actually parses (not just "does the URL contain `cursor=`"). Pages are
|
||||||
* every cursor it serves so the scenario can assert none was requested
|
* deliberately large enough to overflow the list's own scroll container so
|
||||||
* twice — the tell-tale of the issue #38 loop, where a stale closure
|
* the sentinel starts below the fold — each `scrollTo("bottom")` then
|
||||||
* re-requests the same page forever instead of advancing.
|
* 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[] = [];
|
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", () => {
|
Given("browsing TheMealDB returns three pages, each addressed by its own cursor", () => {
|
||||||
threePageCursorsRequested.length = 0;
|
threePageCursorsRequested.length = 0;
|
||||||
const item = (externalId: string, title: string) => ({
|
const pages: Record<string, ReturnType<typeof threePagePage>> = {
|
||||||
externalId,
|
"": threePagePage(1, "cursor-2"),
|
||||||
title,
|
"cursor-2": threePagePage(2, "cursor-3"),
|
||||||
picture: null,
|
"cursor-3": threePagePage(3, 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 },
|
|
||||||
};
|
};
|
||||||
cy.intercept("GET", "**/sources/theMealDb/browse*", (req) => {
|
cy.intercept("GET", "**/sources/theMealDb/browse*", (req) => {
|
||||||
const cursor = new URL(req.url).searchParams.get("cursor") ?? "";
|
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"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Then("no browse cursor should have been requested more than once", () => {
|
When("I scroll the source list to the bottom", () => {
|
||||||
// Let the panel settle (auto-scroll + one-page-ahead prefetch) before
|
cy.get(".source-items-column .recipe-table-wrap").scrollTo("bottom", {
|
||||||
// asserting. A looping IntersectionObserver would re-hit "" or "cursor-2"
|
ensureScrollable: false,
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
// 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", () => {
|
||||||
|
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
|
// The panel prefetches the next page as soon as page 1 is on screen (before
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue