* feat(recipes): ajoute un adaptateur RecipeSourceAdapter pour Marmiton Étend jsonLdRecipeAdapter (json-ld-recipe.ts) plutôt que de dupliquer sa logique : marmitonAdapter délègue fetchDetail/parse directement à l'adaptateur générique JSON-LD (une page recette marmiton.org expose un Recipe schema.org standard), et n'ajoute que ce que l'adaptateur générique ne peut pas offrir — un list() qui lit l'ItemList schema.org embarqué sur la page de résultats de recherche de marmiton.org (pagination via &page=N, fin de résultats détectée via la réponse 404 renvoyée au-delà de la dernière page). extractJsonLdBlocks est exporté depuis json-ld-recipe.ts pour être réutilisé par marmiton.ts sans dupliquer le regex d'extraction des blocs <script type="application/ld+json">. Enregistre marmitonAdapter dans registerAllRecipeSources (sources/index.ts) — contrairement à jsonLdRecipeAdapter lui-même, c'est un adaptateur concret par site, donc une Source household-toggleable légitime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(recipes): ajoute un adaptateur RecipeSourceAdapter pour 750g Suit le même schéma que marmitonAdapter (construit sur jsonLdRecipeAdapter), avec deux différences propres à 750g.com : - list() n'a pas d'ItemList JSON-LD à lire sur ses résultats de recherche (la recherche du site est un widget client-side) — appelle donc directement le endpoint GET que ce widget interroge lui-même en interne (un « moteur de réponse IA » qui renvoie un lot de recettes pour une requête en texte libre), et scrape les cartes de résultat par regex en associant à chaque lien de recette sa dernière image précédente plutôt qu'un zip naïf par index (des images décoratives sans carte associée existent réellement dans ce fragment). Vérifié en direct : demander une « page 2 » revient toujours vide, donc nextCursor vaut toujours null, comme theMealDbAdapter. - parse() ne délègue pas aussi directement à jsonLdRecipeAdapter.parse que marmitonAdapter — le générateur JSON-LD de 750g.com a deux bugs réels : des caractères de contrôle bruts non échappés dans certaines chaînes JSON (~1 recette sur 3 dans un échantillon vérifié en direct, sinon JSON.parse échoue et jsonLdRecipeAdapter rapporte à tort « aucun Recipe trouvé »), et un texte parfois doublement encodé en entités HTML (ex. un vrai « é » devient &eacute; au lieu de é). Les deux sont corrigés en pré/post-traitement autour de la même délégation, pas une réimplémentation. Enregistre sevenFiftyGAdapter dans registerAllRecipeSources (sources/index.ts), au même titre que marmitonAdapter. Complète aussi test/sources/sources-index.test.ts, qui ne couvrait encore que TheMealDB malgré l'ajout de Marmiton dans une PR précédente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(recipes): ajoute un adaptateur RecipeSourceAdapter pour Manger Bouger Suit le même schéma que marmitonAdapter/sevenFiftyGAdapter (construit sur jsonLdRecipeAdapter), avec des différences propres à mangerbouger.fr (« La Fabrique à Menus », Santé publique France) : - list() n'utilise pas de JSON-LD du tout — la page de résultats (une app Next.js) n'embarque aucun ItemList. Elle est cependant rendue côté serveur et expose le même state Redux que le client hydrate, via un <script id="__NEXT_DATA__">, qui contient déjà tout ce dont list() a besoin (slug/nom/image, pagination). Vérifié en direct : ?query=<texte libre> filtre bien côté serveur, et hasMorePages donne un signal de fin de pagination plus propre que le 404 de Marmiton ou l'absence de vraie pagination de 750g. - parse() délègue à jsonLdRecipeAdapter mais corrige deux lacunes réelles et systématiques de son propre JSON-LD (vérifiées sur 9 recettes, 72 étapes) : recipeInstructions[].text est un document Slate.js sérialisé en JSON (pas du texte) plutôt qu'être aplati ; recipeYield est absent partout alors que le nombre de portions existe bien côté site (__NEXT_DATA__) — les deux sont corrigés par un patch structuré (parse → mutation → réécriture) avant délégation, pas une réimplémentation. Enregistre mangerBougerAdapter dans registerAllRecipeSources (sources/index.ts) et complète sources-index.test.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
import { expect } from "chai";
|
|
import {
|
|
clearRecipeSources,
|
|
getRecipeSource,
|
|
listRecipeSources,
|
|
} from "../../src/lib/recipe-sources/recipe-source-registry.js";
|
|
import { registerAllRecipeSources } from "../../src/sources/index.js";
|
|
|
|
// Not exercised by any other test file — `registerAllRecipeSources` is
|
|
// deliberately never imported by `app.ts` (see its own doc comment), so
|
|
// nothing else in the suite triggers it. Registers/clears explicitly here
|
|
// rather than relying on module-load order, so this test's outcome doesn't
|
|
// depend on which other test file Mocha happens to load first.
|
|
describe("registerAllRecipeSources", () => {
|
|
afterEach(() => {
|
|
clearRecipeSources();
|
|
});
|
|
|
|
it("registers TheMealDB into the shared registry", () => {
|
|
registerAllRecipeSources();
|
|
|
|
const theMealDb = getRecipeSource("theMealDb");
|
|
expect(theMealDb).to.not.be.undefined;
|
|
expect(theMealDb?.name).to.equal("TheMealDB");
|
|
expect(listRecipeSources().map((adapter) => adapter.key)).to.include("theMealDb");
|
|
});
|
|
|
|
it("registers Marmiton into the shared registry", () => {
|
|
registerAllRecipeSources();
|
|
|
|
const marmiton = getRecipeSource("marmiton");
|
|
expect(marmiton).to.not.be.undefined;
|
|
expect(marmiton?.name).to.equal("Marmiton");
|
|
expect(listRecipeSources().map((adapter) => adapter.key)).to.include("marmiton");
|
|
});
|
|
|
|
it("registers 750g into the shared registry", () => {
|
|
registerAllRecipeSources();
|
|
|
|
const sevenFiftyG = getRecipeSource("750g");
|
|
expect(sevenFiftyG).to.not.be.undefined;
|
|
expect(sevenFiftyG?.name).to.equal("750g");
|
|
expect(listRecipeSources().map((adapter) => adapter.key)).to.include("750g");
|
|
});
|
|
|
|
it("registers Manger Bouger into the shared registry", () => {
|
|
registerAllRecipeSources();
|
|
|
|
const mangerBouger = getRecipeSource("mangerBouger");
|
|
expect(mangerBouger).to.not.be.undefined;
|
|
expect(mangerBouger?.name).to.equal("Manger Bouger");
|
|
expect(listRecipeSources().map((adapter) => adapter.key)).to.include("mangerBouger");
|
|
});
|
|
|
|
it("does not register the generic JSON-LD adapter — it's not a household-toggleable source in its own right", () => {
|
|
registerAllRecipeSources();
|
|
|
|
expect(getRecipeSource("jsonLdRecipe")).to.be.undefined;
|
|
expect(listRecipeSources().map((adapter) => adapter.key)).to.not.include("jsonLdRecipe");
|
|
});
|
|
});
|