fix(sources): Manger Bouger sans filtre déroule tout le catalogue (#78)
All checks were successful
CI / lint (push) Successful in 2m48s
CI / build (push) Successful in 2m53s
CI / intent-service-test (push) Successful in 4m29s
CI / e2e (push) Successful in 9m49s
CI / test (push) Successful in 18m54s
CI / build-android (push) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-08 20:22:25 +02:00
parent 46c23c24ab
commit 7d20404b6f
2 changed files with 73 additions and 8 deletions

View file

@ -18,11 +18,13 @@ const SOURCE_KEY = "mangerBouger";
// all (unlike marmiton.ts's search page) — but it's server-rendered, and a // 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 // plain GET carries the exact same Redux state the client hydrates from as
// a `__NEXT_DATA__` script tag (see `extractNextData` below), which already // a `__NEXT_DATA__` script tag (see `extractNextData` below), which already
// has everything `list()` needs. Verified live: `?query=<free text>` really // has everything `list()` needs. Verified live for the *search* path
// filters server-side (not just a client-side URL update over an // (`?diet=ALL&page=N&query=<free text>`): `query` really filters
// already-fetched page), and `page`/`hasMorePages` behave as real, // server-side (not just a client-side URL update over an already-fetched
// consistent pagination — the best-behaved of this adapter family's three // page), and `page`/`hasMorePages` behave as real, consistent pagination.
// sources on that front. // 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 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/"; const DETAIL_BASE_URL = "https://www.mangerbouger.fr/manger-mieux/la-fabrique-a-menus/recettes/";
@ -42,7 +44,7 @@ interface MangerBougerListPageData {
initialState?: { initialState?: {
recipes?: { recipes?: {
list?: MangerBougerListEntry[]; 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; hasMorePages?: boolean;
}; };
}; };
@ -284,8 +286,18 @@ export const mangerBougerAdapter: RecipeSourceAdapter<{ html: string; url: strin
async list(params: RecipeSourceListParams): Promise<RecipeSourceListResult> { async list(params: RecipeSourceListParams): Promise<RecipeSourceListResult> {
try { try {
const page = params.cursor ? Number(params.cursor) : 1; const page = params.cursor ? Number(params.cursor) : 1;
const query = params.query ?? ""; // Only append `&query=` when there's an actual search term. A trailing
const listUrl = `${LIST_URL}?diet=ALL&page=${page}&query=${encodeURIComponent(query)}`; // 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; let response: Response;
try { try {

View file

@ -132,6 +132,59 @@ describe("mangerBougerAdapter", () => {
expect(requestedUrl).to.include("query=cr%C3%A8me%20br%C3%BBl%C3%A9e"); 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 () => { it("skips a list entry missing a slug or a name", async () => {
stubFetchHtml( stubFetchHtml(
htmlWithListNextData({ htmlWithListNextData({