From 7d20404b6f9479118507bc72d56d9453f0c4efb6 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 8 Sep 2026 20:22:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(sources):=20Manger=20Bouger=20sans=20filtre?= =?UTF-8?q?=20d=C3=A9roule=20tout=20le=20catalogue=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le parcours « tout voir » (filtre texte vide) de la source Manger Bouger s'arrêtait après la 1re page (9 recettes) sur le serveur déployé. Cause : `list()` ajoutait toujours un `&query=` final, même vide. Sur le serveur déployé, `…/recettes?diet=ALL&page=N&query=` renvoyait la page 1 pour tout `N`, et le garde-fou anti-boucle de #38 (« 0 nouvel externalId ⇒ fin de liste ») coupait le défilement. La requête catalogue nue `?diet=ALL&page=N` pagine correctement. - `manger-bouger.ts` `list()` : n'ajoute `&query=` que si le terme de recherche est non vide (après `trim`), comme les adaptateurs voisins (marmiton / quitoque / ohmonbento). Le chemin filtré ne change pas. - Commentaires `LIST_URL` / `hasMorePages` corrigés : la pagination « vérifiée en direct » ne valait que pour le chemin avec `query` texte. - Tests `manger-bouger.test.ts` : `list({})` construit une URL sans paramètre `query` ; un `query` blanc = pas de filtre ; le catalogue nu pagine quand même (`nextCursor`) ; `list({ query: salade })` ajoute toujours `&query=salade` (non-régression). Closes #78. Co-Authored-By: Claude Sonnet 5 --- apps/api/src/sources/manger-bouger.ts | 28 +++++++--- .../test/recipe-sources/manger-bouger.test.ts | 53 +++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/apps/api/src/sources/manger-bouger.ts b/apps/api/src/sources/manger-bouger.ts index ccf7dff..b0ed9fc 100644 --- a/apps/api/src/sources/manger-bouger.ts +++ b/apps/api/src/sources/manger-bouger.ts @@ -18,11 +18,13 @@ const SOURCE_KEY = "mangerBouger"; // all (unlike marmiton.ts's search page) — but it's server-rendered, and a // plain GET carries the exact same Redux state the client hydrates from as // a `__NEXT_DATA__` script tag (see `extractNextData` below), which already -// has everything `list()` needs. Verified live: `?query=` really -// filters server-side (not just a client-side URL update over an -// already-fetched page), and `page`/`hasMorePages` behave as real, -// consistent pagination — the best-behaved of this adapter family's three -// sources on that front. +// has everything `list()` needs. Verified live for the *search* path +// (`?diet=ALL&page=N&query=`): `query` really filters +// server-side (not just a client-side URL update over an already-fetched +// page), and `page`/`hasMorePages` behave as real, consistent pagination. +// The *catalogue browse* path (no `query` term) must be the bare +// `?diet=ALL&page=N` — a trailing empty `&query=` made the deployed server +// repeat page 1 for every page (issue #78); `list()` below now omits it. const LIST_URL = "https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes"; const DETAIL_BASE_URL = "https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes/"; @@ -42,7 +44,7 @@ interface MangerBougerListPageData { initialState?: { recipes?: { list?: MangerBougerListEntry[]; - /** Whether a further page exists for the current `page`/`query`/`diet` combination — verified live: an out-of-range page comes back `false` with an empty `list` rather than repeating the last page or erroring, a cleaner end-of-results signal than either `marmiton.ts` (infers it from a 404) or `750g.ts` (this search has no real pagination at all). */ + /** Whether a further page exists for the current `page`/`query`/`diet` combination — verified live on the `query`-term path: an out-of-range page comes back `false` with an empty `list` rather than repeating the last page or erroring, a cleaner end-of-results signal than either `marmiton.ts` (infers it from a 404) or `750g.ts` (this search has no real pagination at all). The catalogue-browse path (no `query`) only paginates when the request URL omits `&query=` entirely — see `LIST_URL` and issue #78. */ hasMorePages?: boolean; }; }; @@ -284,8 +286,18 @@ export const mangerBougerAdapter: RecipeSourceAdapter<{ html: string; url: strin async list(params: RecipeSourceListParams): Promise { try { const page = params.cursor ? Number(params.cursor) : 1; - const query = params.query ?? ""; - const listUrl = `${LIST_URL}?diet=ALL&page=${page}&query=${encodeURIComponent(query)}`; + // Only append `&query=` when there's an actual search term. A trailing + // empty `&query=` on the "browse everything" path (filter absent) made + // the *deployed* server serve page 1 for every `page=N` — the + // infinite scroll then stopped after 9 recipes (issue #38's + // "0 new externalIds ⇒ end of list" guard cutting in). The bare + // `?diet=ALL&page=N` catalogue request paginates correctly. The + // sibling adapters (marmiton/quitoque/ohmonbento) already make their + // search param conditional; this one didn't. + const query = (params.query ?? "").trim(); + const listUrl = `${LIST_URL}?diet=ALL&page=${page}${ + query ? `&query=${encodeURIComponent(query)}` : "" + }`; let response: Response; try { diff --git a/apps/api/test/recipe-sources/manger-bouger.test.ts b/apps/api/test/recipe-sources/manger-bouger.test.ts index a5cc209..9e0c076 100644 --- a/apps/api/test/recipe-sources/manger-bouger.test.ts +++ b/apps/api/test/recipe-sources/manger-bouger.test.ts @@ -132,6 +132,59 @@ describe("mangerBougerAdapter", () => { expect(requestedUrl).to.include("query=cr%C3%A8me%20br%C3%BBl%C3%A9e"); }); + it("omits the query param entirely on the catalogue-browse path (no filter) — issue #78", async () => { + let requestedUrl: string | undefined; + globalThis.fetch = (async (url: string) => { + requestedUrl = url; + return new Response(htmlWithListNextData({ list: [], hasMorePages: true }), { + status: 200, + }); + }) as typeof fetch; + + await mangerBougerAdapter.list({}); + + expect(requestedUrl).to.equal( + "https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes?diet=ALL&page=1", + ); + expect(requestedUrl).not.to.include("query="); + }); + + it("treats a blank/whitespace query the same as no filter (no query param)", async () => { + let requestedUrl: string | undefined; + globalThis.fetch = (async (url: string) => { + requestedUrl = url; + return new Response(htmlWithListNextData({ list: [], hasMorePages: false }), { + status: 200, + }); + }) as typeof fetch; + + await mangerBougerAdapter.list({ query: " " }); + + expect(requestedUrl).not.to.include("query="); + }); + + it("still paginates the catalogue-browse path (nextCursor set when hasMorePages)", async () => { + stubFetchHtml(htmlWithListNextData({ list: [], hasMorePages: true })); + + const result = await mangerBougerAdapter.list({}); + + expect(result.nextCursor).to.equal("2"); + }); + + it("still appends the query param when a real search term is given", async () => { + let requestedUrl: string | undefined; + globalThis.fetch = (async (url: string) => { + requestedUrl = url; + return new Response(htmlWithListNextData({ list: [], hasMorePages: false }), { + status: 200, + }); + }) as typeof fetch; + + await mangerBougerAdapter.list({ query: "salade" }); + + expect(requestedUrl).to.include("&query=salade"); + }); + it("skips a list entry missing a slug or a name", async () => { stubFetchHtml( htmlWithListNextData({ -- 2.45.2