From b103ab78ecfd0c507255e0478b3b4179e12ad0db Mon Sep 17 00:00:00 2001 From: Pierre Martin Date: Fri, 17 Jul 2026 13:07:17 +0200 Subject: [PATCH] feat: introduce a shared config with an optional local override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mairie form needs an organiser, an email and the café defaults. Ship the defaults in the repo and let each volunteer override the personal bits in config.local.json (gitignored, optional: a fresh clone still works). config.js carries neither import nor export on purpose, so that it loads both as a classic script (content script, background) and as an ESM module. That is what lets presentation.js drop its duplicated café address. Co-Authored-By: Claude Opus 4.8 --- config.test.ts | 63 +++++++++++++++++++++++++++++ extension/config.js | 39 ++++++++++++++++++ extension/config.local.json.example | 4 ++ extension/presentation.js | 9 ++++- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 config.test.ts create mode 100644 extension/config.js create mode 100644 extension/config.local.json.example diff --git a/config.test.ts b/config.test.ts new file mode 100644 index 0000000..e08fd84 --- /dev/null +++ b/config.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test"; +// config.js n'exporte RIEN (chargeable en script classique ET en module) : +// on l'importe pour son effet de bord et on lit le namespace global. +import "./extension/config.js"; + +const { DEFAUTS, fusionner, CLE_CONFIG_LOCALE } = (globalThis as any).EchoConfig; + +describe("DEFAUTS", () => { + test("themeDefaut est une valeur exacte du dropdown mairie (RECHERCHE §3)", () => { + expect(DEFAUTS.themeDefaut).toBe("Culture"); + }); + + test("organisateur et email sont vides : jamais inventés, signalés à l'utilisateur", () => { + expect(DEFAUTS.organisateur).toBe(""); + expect(DEFAUTS.email).toBe(""); + }); + + test("le café porte l'adresse et les coordonnées de RECHERCHE §2", () => { + expect(DEFAUTS.cafe.adresse).toBe("8 Rue du Pré Vicinal 31270 Cugnaux"); + expect(DEFAUTS.cafe.coords).toBe("43.53753132806989|1.342981890597508"); + }); + + test("tarifs par défaut : Gratuit", () => { + expect(DEFAUTS.tarifs).toBe("Gratuit"); + }); +}); + +describe("CLE_CONFIG_LOCALE", () => { + test("est la clé de storage partagée background → content script", () => { + expect(CLE_CONFIG_LOCALE).toBe("config-locale"); + }); +}); + +describe("fusionner", () => { + // Un clone frais n'a pas de config.local.json : le repli doit être total, + // jamais une exception. + test.each([ + ["absente", undefined], + ["null", null], + ["une chaîne", "n'importe quoi"], + ["un tableau vide", []], + ])("locale %s → défauts nus", (_libelle, locale) => { + expect(fusionner(DEFAUTS, locale)).toEqual(DEFAUTS); + }); + + test("organisateur et email surchargent, le reste survit", () => { + const fusion = fusionner(DEFAUTS, { + organisateur: "L'Atelier du Huit", + email: "contact@atelier-huit.fr", + }); + + expect(fusion.organisateur).toBe("L'Atelier du Huit"); + expect(fusion.email).toBe("contact@atelier-huit.fr"); + expect(fusion.themeDefaut).toBe("Culture"); + expect(fusion.cafe).toEqual(DEFAUTS.cafe); + }); + + test("ne mute pas DEFAUTS (le module est partagé par tout le monde)", () => { + fusionner(DEFAUTS, { organisateur: "Autre" }); + + expect(DEFAUTS.organisateur).toBe(""); + }); +}); diff --git a/extension/config.js b/extension/config.js new file mode 100644 index 0000000..bb03a75 --- /dev/null +++ b/extension/config.js @@ -0,0 +1,39 @@ +// Défauts committés de l'extension + fusion avec la surcharge locale. +// +// ⚠️ Ce fichier n'a NI `import` NI `export`, DÉLIBÉRÉMENT (ne pas « corriger ») : +// c'est ce qui le rend chargeable à la fois comme content script / script de +// background (scripts classiques : un `export` y lève une SyntaxError) et comme +// module ESM (`import "./config.js"` depuis presentation.js et bun test). +// Il communique donc par `globalThis`. Cf. DECISIONS.md 2026-07-17. + +globalThis.EchoConfig = { + // Clé de storage : le background écrit, le content script lit. + CLE_CONFIG_LOCALE: "config-locale", + + DEFAUTS: { + // Vides par défaut : sans config.local.json, on préfère un champ vide + // signalé dans le bandeau à un organisateur inventé. + organisateur: "", + email: "", + + // RECHERCHE.md §3 : valeur exacte du dropdown #input_6_20. + themeDefaut: "Culture", + tarifs: "Gratuit", + + // RECHERCHE.md §2. `input_18` se compose `${coords}|${adresse}`. + cafe: { + adresse: "8 Rue du Pré Vicinal 31270 Cugnaux", + coords: "43.53753132806989|1.342981890597508", + }, + }, + + // Fusion de surface : la locale ne porte que des champs plats + // (organisateur, email…). Tolérante par nécessité — `config.local.json` est + // optionnel et éditable à la main : absent, `null` ou invalide → défauts nus, + // jamais d'exception. + fusionner(defauts, locale) { + const surcharge = + locale && typeof locale === "object" && !Array.isArray(locale) ? locale : {}; + return { ...defauts, ...surcharge }; + }, +}; diff --git a/extension/config.local.json.example b/extension/config.local.json.example new file mode 100644 index 0000000..dc767b5 --- /dev/null +++ b/extension/config.local.json.example @@ -0,0 +1,4 @@ +{ + "organisateur": "L'Atelier du Huit", + "email": "contact@atelier-huit.fr" +} diff --git a/extension/presentation.js b/extension/presentation.js index b357d53..0f107f8 100644 --- a/extension/presentation.js +++ b/extension/presentation.js @@ -2,11 +2,16 @@ // Fonctions pures, sans DOM (testables). Locale et fuseau sont des constantes : // l'appli est mono-locale (bénévoles de Cugnaux). +// config.js n'exporte rien : il pose globalThis.EchoConfig (cf. DECISIONS.md +// 2026-07-17). L'import ne sert qu'à son effet de bord. +import "./config.js"; + const LOCALE = "fr-FR"; const FUSEAU = "Europe/Paris"; -// RECHERCHE.md §2 : adresse du café de l'Atelier du Huit. -export const LIEU_PAR_DEFAUT = "8 Rue du Pré Vicinal 31270 Cugnaux"; +// Source unique : le formulaire mairie a besoin de la même adresse (et de ses +// coordonnées) que la liste. +export const LIEU_PAR_DEFAUT = globalThis.EchoConfig.DEFAUTS.cafe.adresse; // Un événement horodaté porte un vrai instant : on l'affiche à l'heure de // Cugnaux, quel que soit le fuseau de la machine.