corentin_wakdo/tests/js/composer-slots.test.js
Imugiii b3521f7a56
All checks were successful
CI / secret-scan (push) Successful in 18s
CI / js-tests (pull_request) Successful in 38s
CI / php-lint (push) Successful in 42s
CI / static-tests (push) Successful in 1m29s
CI / js-tests (push) Successful in 45s
CI / secret-scan (pull_request) Successful in 17s
CI / php-lint (pull_request) Successful in 40s
CI / static-tests (pull_request) Successful in 1m10s
feat(borne): menu Maxi agrandit la boisson en 50cl + transport explicite du format
Seed 0006 lie chaque soda fontaine 30cl a sa variante 50cl via
maxi_variant_product_id : en menu Maxi, resolveSelections substitue la
boisson vers la 50cl (meme mecanique que l'accompagnement Grande Frite),
sans code serveur. Les boissons en bouteille (sans variante) restent en
taille standard, le surcout Maxi etant porte par le menu.

La borne transporte desormais le format Normal/Maxi explicitement
(buildMenuCartItem) au lieu de l'inferer de supplement_cents>0 (faux
negatif si maxi==normal) ; checkout.js lit cartItem.format avec repli
historique pour les paniers serialises. Commentaire migration 0007
corrige (la substitution Maxi de la boisson est desormais voulue).

Tests : OrderRepositoryTest (boisson Maxi -> 50cl + bouteille inchangee),
checkout/composer-slots (format transporte). Seed valide idempotent sur
base jetable (5 sodas lies, frites intactes, bouteilles NULL).
2026-06-24 08:57:53 +00:00

168 lines
8.8 KiB
JavaScript

/*
* Tests du composeur de menu slot-driven (P5 L2), node:test + jsdom.
*
* page-product-menu.js importe nav.js (qui touche le DOM au chargement) -> import
* dynamique apres pose des globals jsdom. Cible : fonctions PURES buildComposerSteps,
* buildMenuCartItem, selectionsComplete (logique slots -> etapes -> item panier).
*/
import { test, before } from 'node:test';
import assert from 'node:assert/strict';
import { JSDOM } from 'jsdom';
let buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable, optionLabel;
before(async () => {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://kiosk.test/product.html' });
global.window = dom.window;
global.document = dom.window.document;
global.localStorage = dom.window.localStorage;
({ buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable, optionLabel } =
await import('../../src/public/borne/assets/js/page-product-menu.js'));
});
const detail = () => ({
id: 1,
burger_product_id: 100,
price_normal_cents: 880,
price_maxi_cents: 1030,
slots: [
{ id: 16, name: 'Accompagnement', slot_type: 'side', is_required: true, display_order: 2, option_product_ids: [22, 23] },
{ id: 1, name: 'Boisson', slot_type: 'drink', is_required: true, display_order: 1, option_product_ids: [14, 15, 999] },
{ id: 31, name: 'Sauce', slot_type: 'sauce', is_required: false, display_order: 3, option_product_ids: [47] },
],
});
const byId = () => ({
100: { id: 100, nom: 'Le 280', prix: 0, image: 'b.png', type: 'produit', maxiNom: null },
// Accompagnements : variante Maxi (maxiNom) renseignee -> agrandissable.
22: { id: 22, nom: 'Moyenne Frite', prix: 0, image: 'f.png', type: 'produit', maxiNom: 'Grande Frite' },
23: { id: 23, nom: 'Potatoes', prix: 0, image: 'p.png', type: 'produit', maxiNom: 'Grande Potatoes' },
// Boissons : pas de variante Maxi (le menu Maxi n'agrandit pas la boisson).
14: { id: 14, nom: 'Coca', prix: 0, image: 'c.png', type: 'produit', maxiNom: null },
15: { id: 15, nom: 'Eau', prix: 0, image: 'e.png', type: 'produit', maxiNom: null },
47: { id: 47, nom: 'Ketchup', prix: 0, image: 'k.png', type: 'produit', maxiNom: null },
});
const menu = { id: 1, nom: 'Menu Le 280', image: 'b.png', type: 'menu' };
/* --- buildComposerSteps -------------------------------------------------- */
test('buildComposerSteps: burger impose resolu, slots tries par display_order', () => {
const m = buildComposerSteps(detail(), byId());
assert.equal(m.burger.nom, 'Le 280');
assert.equal(m.priceNormalCents, 880);
assert.equal(m.priceMaxiCents, 1030);
assert.deepEqual(m.slots.map(s => s.slotType), ['drink', 'side', 'sauce']); // par display_order 1,2,3
});
test('buildComposerSteps: option_product_ids resolus en produits, ids inconnus filtres', () => {
const m = buildComposerSteps(detail(), byId());
const drink = m.slots.find(s => s.slotType === 'drink');
assert.deepEqual(drink.options.map(o => o.nom), ['Coca', 'Eau']); // 999 inconnu -> filtre
assert.equal(drink.isRequired, true);
assert.equal(m.slots.find(s => s.slotType === 'sauce').isRequired, false);
});
/* --- buildMenuCartItem --------------------------------------------------- */
test('buildMenuCartItem Normal: prix normal, pas de supplement, taille N, composition mappee', () => {
const m = buildComposerSteps(detail(), byId());
const item = buildMenuCartItem(menu, m, { size: 'N', selections: { 1: 14, 16: 22, 31: 47 } });
assert.equal(item.type, 'menu');
assert.equal(item.prix_cents, 880);
assert.equal(item.supplement_cents, 0);
assert.equal(item.format, 'normal'); // format explicite transporte
assert.equal(item.composition.burger.libelle, 'Le 280');
// Normal : l'accompagnement garde son nom de base (pas la variante Maxi).
assert.deepEqual(item.composition.accompagnement, { id: 22, libelle: 'Moyenne Frite', taille: 'N' });
assert.deepEqual(item.composition.boisson, { id: 14, libelle: 'Coca', taille: 'N' });
assert.deepEqual(item.composition.sauce, { id: 47, libelle: 'Ketchup' });
});
test('buildMenuCartItem Maxi: supplement = maxi - normal, taille G sur side/drink', () => {
const m = buildComposerSteps(detail(), byId());
const item = buildMenuCartItem(menu, m, { size: 'M', selections: { 1: 14, 16: 22, 31: 47 } });
assert.equal(item.prix_cents, 880);
assert.equal(item.supplement_cents, 150); // 1030 - 880
assert.equal(item.format, 'maxi'); // format explicite transporte
assert.equal(item.composition.accompagnement.taille, 'G');
assert.equal(item.composition.boisson.taille, 'G');
});
test('buildMenuCartItem Maxi: l accompagnement prend sa variante (Grande Frite), pas le nom de base', () => {
const m = buildComposerSteps(detail(), byId());
const item = buildMenuCartItem(menu, m, { size: 'M', selections: { 1: 14, 16: 22, 31: 47 } });
assert.equal(item.composition.accompagnement.libelle, 'Grande Frite'); // pas "Moyenne Frite"
// Boisson sans maxiNom : garde son nom de base meme en Maxi (cas bouteille).
assert.equal(item.composition.boisson.libelle, 'Coca');
});
test('buildMenuCartItem Maxi: la boisson AVEC variante (50cl) prend son nom agrandi', () => {
// Apres le seed 0006, une boisson fontaine porte maxiNom (ex. "Coca Cola 50cl") :
// en Maxi, le libelle et la taille refletent la grande boisson (meme regle que
// l'accompagnement). Aucune logique borne specifique : maxiNom suffit.
const byIdDrinkVariant = { ...byId(), 14: { id: 14, nom: 'Coca Cola', prix: 0, image: 'c.png', type: 'produit', maxiNom: 'Coca Cola 50cl' } };
const m = buildComposerSteps(detail(), byIdDrinkVariant);
const item = buildMenuCartItem(menu, m, { size: 'M', selections: { 1: 14, 16: 22, 31: 47 } });
assert.equal(item.composition.boisson.libelle, 'Coca Cola 50cl');
assert.equal(item.composition.boisson.taille, 'G');
});
test('buildMenuCartItem Normal: l accompagnement garde "Moyenne Frite" (pas de variante)', () => {
const m = buildComposerSteps(detail(), byId());
const item = buildMenuCartItem(menu, m, { size: 'N', selections: { 1: 14, 16: 22, 31: 47 } });
assert.equal(item.composition.accompagnement.libelle, 'Moyenne Frite');
});
/* --- optionLabel (pur) : libelle affiche au CHOIX selon le format -------- */
test('optionLabel: Maxi affiche la variante quand elle existe, sinon le nom de base', () => {
const frite = { nom: 'Moyenne Frite', maxiNom: 'Grande Frite' };
const coca = { nom: 'Coca', maxiNom: null };
assert.equal(optionLabel(frite, 'M'), 'Grande Frite');
assert.equal(optionLabel(frite, 'N'), 'Moyenne Frite');
assert.equal(optionLabel(coca, 'M'), 'Coca'); // pas de variante -> nom de base
assert.equal(optionLabel(coca, 'N'), 'Coca');
});
test('buildMenuCartItem: slot optionnel non choisi -> champ absent de composition', () => {
const m = buildComposerSteps(detail(), byId());
const item = buildMenuCartItem(menu, m, { size: 'N', selections: { 1: 14, 16: 22 } }); // pas de sauce
assert.equal(item.composition.sauce, undefined);
assert.ok(item.composition.accompagnement);
assert.ok(item.composition.boisson);
});
/* --- selectionsComplete -------------------------------------------------- */
test('selectionsComplete: vrai si tous les slots REQUIS sont choisis (sauce optionnelle ignoree)', () => {
const m = buildComposerSteps(detail(), byId());
assert.equal(selectionsComplete(m, { 1: 14, 16: 22 }), true); // requis ok, sauce absente
assert.equal(selectionsComplete(m, { 1: 14 }), false); // accompagnement requis manquant
assert.equal(selectionsComplete(m, { 1: 14, 16: 999 }), false); // id hors options du slot
});
/* --- garde-fous (findings revue L2) -------------------------------------- */
test('buildComposerSteps: ignore les slot_type hors {drink,side,sauce} (anti-perte silencieuse)', () => {
const d = detail();
d.slots.push({ id: 99, name: 'Dessert', slot_type: 'dessert', is_required: true, display_order: 4, option_product_ids: [22] });
const m = buildComposerSteps(d, byId());
assert.deepEqual(m.slots.map(s => s.slotType), ['drink', 'side', 'sauce']); // dessert exclu
});
test('composerIsViable: vrai pour un modele complet', () => {
assert.equal(composerIsViable(buildComposerSteps(detail(), byId())), true);
});
test('composerIsViable: faux si un slot requis n a aucune option resolue', () => {
const d = detail();
d.slots = [{ id: 1, name: 'Boisson', slot_type: 'drink', is_required: true, display_order: 1, option_product_ids: [999, 888] }];
assert.equal(composerIsViable(buildComposerSteps(d, byId())), false);
});
test('composerIsViable: faux si le burger impose est introuvable', () => {
const d = detail();
d.burger_product_id = 12345;
assert.equal(composerIsViable(buildComposerSteps(d, byId())), false);
});