import { expect } from "chai"; import { RecipeSourceFetchError, RecipeSourceParseError, } from "../../src/lib/recipe-sources/recipe-source-errors.js"; import { quitoqueAdapter } from "../../src/sources/quitoque.js"; /** Stubs `globalThis.fetch` to return `html` — same pattern as `marmiton.test.ts`/`750g.test.ts`. */ function stubFetchHtml(html: string, status = 200) { globalThis.fetch = (async () => new Response(html, { status })) as typeof fetch; } /** Like {@link stubFetchHtml} but records the URL each call was made with — for asserting `list()`'s pagination. */ function stubFetchHtmlCapturing(html: string): { calls: string[] } { const calls: string[] = []; globalThis.fetch = (async (input: string | URL | Request) => { calls.push(typeof input === "string" ? input : input.toString()); return new Response(html, { status: 200 }); }) as typeof fetch; return { calls }; } /** A `/recettes` listing page carrying `count` cards, each a single `` wrapping an ``. */ function listingHtml(cards: { slug: string; title: string; image: string }[]): string { const anchors = cards .map( (card) => `
` + `${card.title}` + `
`, ) .join("\n"); return `
${anchors}
`; } /** A quitoque.fr recipe page: a plain schema.org `Recipe` as the sole element of a top-level JSON-LD array. */ function detailHtml(): string { const payload = [ { "@context": "https://schema.org", "@type": "Recipe", name: "Mijoté de lentilles au lait de coco", description: "Un plat végétarien réconfortant.", image: "https://www.quitoque.fr/img/mijote.jpg", recipeYield: "2", recipeIngredient: ["150 g lentilles vertes (Bio)", "1 gousse d'ail", "0.5 citron jaune"], recipeInstructions: [ { "@type": "HowToStep", name: "1. Les légumes", text: "Émincez l'ail." }, { "@type": "HowToStep", name: "2. La cuisson", text: "Faites mijoter 25 minutes." }, ], }, ]; return ``; } describe("quitoqueAdapter", () => { let originalFetch: typeof fetch; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; }); it("declares itself as an unofficial, French-locale source with an icon", () => { expect(quitoqueAdapter.key).to.equal("quitoque"); expect(quitoqueAdapter.name).to.equal("Quitoque"); expect(quitoqueAdapter.official).to.equal(false); expect(quitoqueAdapter.iconUrl).to.be.a("string"); expect(quitoqueAdapter.locale).to.equal("fr"); }); describe("list", () => { it("scrapes each card into a RecipeSourceListItem (real image from data-src, absolute url) and offers a next page", async () => { stubFetchHtml( listingHtml([ { slug: "mijote-lentilles", title: "Mijoté de lentilles", image: "https://img.test/1.jpg", }, { slug: "curry-legumes", title: "Curry de légumes", image: "https://img.test/2.jpg" }, ]), ); const result = await quitoqueAdapter.list({}); expect(result.items).to.deep.equal([ { externalId: "https://www.quitoque.fr/recettes/mijote-lentilles", title: "Mijoté de lentilles", picture: "https://img.test/1.jpg", url: "https://www.quitoque.fr/recettes/mijote-lentilles", }, { externalId: "https://www.quitoque.fr/recettes/curry-legumes", title: "Curry de légumes", picture: "https://img.test/2.jpg", url: "https://www.quitoque.fr/recettes/curry-legumes", }, ]); expect(result.nextCursor).to.equal("2"); }); it("treats a page with no cards as the end of the list", async () => { stubFetchHtml("
"); const result = await quitoqueAdapter.list({ cursor: "9" }); expect(result.items).to.deep.equal([]); expect(result.nextCursor).to.be.null; }); it("follows the cursor to `/recettes?page=N`", async () => { const { calls } = stubFetchHtmlCapturing( listingHtml([{ slug: "x", title: "X", image: "https://img.test/x.jpg" }]), ); await quitoqueAdapter.list({ cursor: "3" }); expect(calls).to.deep.equal(["https://www.quitoque.fr/recettes?page=3"]); }); it("best-effort filters a page's cards by a query substring, but still advances if the raw page had cards", async () => { stubFetchHtml( listingHtml([ { slug: "poulet-curry", title: "Poulet au curry", image: "https://img.test/p.jpg" }, { slug: "boeuf-carottes", title: "Bœuf carottes", image: "https://img.test/b.jpg" }, ]), ); const result = await quitoqueAdapter.list({ query: "poulet" }); expect(result.items.map((item) => item.title)).to.deep.equal(["Poulet au curry"]); // The page did have cards, so the next page might match too. expect(result.nextCursor).to.equal("2"); }); it("throws RecipeSourceFetchError keyed 'quitoque' on a non-2xx listing response", async () => { stubFetchHtml("", 503); try { await quitoqueAdapter.list({}); expect.fail("expected list to throw"); } catch (err) { expect(err).to.be.instanceOf(RecipeSourceFetchError); expect((err as RecipeSourceFetchError).sourceKey).to.equal("quitoque"); } }); }); describe("fetchDetail / parse", () => { it("parses a quitoque.fr recipe page's top-level-array JSON-LD into a ParsedRecipe", async () => { stubFetchHtml(detailHtml()); const raw = await quitoqueAdapter.fetchDetail( "https://www.quitoque.fr/recettes/mijote-lentilles", ); const parsed = quitoqueAdapter.parse(raw); expect(parsed.name).to.equal("Mijoté de lentilles au lait de coco"); expect(parsed.portions).to.equal(2); expect(parsed.ingredients.map((i) => i.rawText)).to.deep.equal([ "150 g lentilles vertes (Bio)", "1 gousse d'ail", "0.5 citron jaune", ]); expect(parsed.steps.map((s) => s.description)).to.deep.equal([ "Émincez l'ail.", "Faites mijoter 25 minutes.", ]); }); it("re-keys a delegated RecipeSourceFetchError to 'quitoque' when the detail page 404s", async () => { stubFetchHtml("", 404); try { await quitoqueAdapter.fetchDetail("https://www.quitoque.fr/recettes/gone"); expect.fail("expected fetchDetail to throw"); } catch (err) { expect(err).to.be.instanceOf(RecipeSourceFetchError); expect((err as RecipeSourceFetchError).sourceKey).to.equal("quitoque"); } }); it("re-keys a delegated RecipeSourceParseError to 'quitoque' when a page has no Recipe JSON-LD", () => { try { quitoqueAdapter.parse({ html: "rien ici", url: "https://www.quitoque.fr/recettes/vide", }); expect.fail("expected parse to throw"); } catch (err) { expect(err).to.be.instanceOf(RecipeSourceParseError); expect((err as RecipeSourceParseError).sourceKey).to.equal("quitoque"); } }); }); });