fix(borne): affiche la variante Grande de l'accompagnement en menu Maxi
All checks were successful
CI / php-lint (push) Successful in 25s
CI / js-tests (push) Successful in 27s
CI / static-tests (pull_request) Successful in 52s
CI / js-tests (pull_request) Successful in 27s
CI / secret-scan (push) Successful in 10s
CI / static-tests (push) Successful in 52s
CI / secret-scan (pull_request) Successful in 10s
CI / php-lint (pull_request) Successful in 23s

Un menu commande en Maxi affichait "Moyenne Frite grande" : le nom de base
de l'accompagnement (Moyenne) plus un suffixe " grande" trompeur. Le serveur
substitue deja Moyenne -> Grande a la commande (maxi_variant_product_id) ;
seul l'affichage borne mentait.

Fix d'affichage + petite extension API pour l'alimenter :
- API : ProductRepository expose mv.name AS maxi_variant_name via un LEFT JOIN
  sur la variante Maxi (liste + detail) ; CatalogueController::presentProduct
  emet maxi_variant_name (null sans variante).
- data.js : mappe maxi_variant_name -> maxiNom sur le produit borne.
- page-product-menu.js : le composeur affiche maxiNom en Maxi au moment du
  CHOIX (optionLabel) ; buildMenuCartItem pose libelle = variante en Maxi.
- order-panel.js / page-cart.js : suppression du suffixe " grande" -- le
  libelle porte deja le bon nom ; le suffixe doublait ("Grande Frite grande")
  et mentait pour la boisson (le menu Maxi ne l'agrandit pas).

La resolution serveur (OrderRepository::resolveSelections) est inchangee.
Tests JS et PHP unit etendus ; taille reste pose mais n'est plus affiche.
This commit is contained in:
Imugiii 2026-06-22 14:15:11 +00:00
parent 042c30a5fe
commit 68b3f9c46c
10 changed files with 149 additions and 31 deletions

View file

@ -75,10 +75,16 @@ final class ProductRepository
*/ */
public function availableForCatalogue(): array public function availableForCatalogue(): array
{ {
// mv.name (LEFT JOIN sur la variante Maxi) : la borne affiche ce nom quand le
// menu est commande en Maxi, sans refaire un aller-retour pour resoudre la
// variante. NULL si le produit n'a pas de variante Maxi. La SUBSTITUTION reelle
// a la commande reste serveur (OrderRepository::resolveSelections) ; ici c'est
// un libelle d'affichage seulement.
return $this->db->fetchAll( return $this->db->fetchAll(
'SELECT p.id, p.category_id, p.name, p.description, p.price_cents, p.size_cl, ' 'SELECT p.id, p.category_id, p.name, p.description, p.price_cents, p.size_cl, '
. 'p.image_path, p.display_order ' . 'p.image_path, p.display_order, mv.name AS maxi_variant_name '
. 'FROM product p JOIN category c ON c.id = p.category_id ' . 'FROM product p JOIN category c ON c.id = p.category_id '
. 'LEFT JOIN product mv ON mv.id = p.maxi_variant_product_id '
. 'WHERE p.is_available = 1 AND c.is_active = 1 AND p.base_product_id IS NULL ' . 'WHERE p.is_available = 1 AND c.is_active = 1 AND p.base_product_id IS NULL '
. 'ORDER BY p.display_order, p.name', . 'ORDER BY p.display_order, p.name',
); );
@ -158,10 +164,13 @@ final class ProductRepository
*/ */
public function findForCatalogue(int $id): ?array public function findForCatalogue(int $id): ?array
{ {
// Meme projection (et meme LEFT JOIN variante Maxi) que la liste : la borne
// recoit maxi_variant_name aussi par lien direct (NULL si pas de variante).
return $this->db->fetch( return $this->db->fetch(
'SELECT p.id, p.category_id, p.name, p.description, p.price_cents, ' 'SELECT p.id, p.category_id, p.name, p.description, p.price_cents, '
. 'p.image_path, p.display_order ' . 'p.image_path, p.display_order, mv.name AS maxi_variant_name '
. 'FROM product p JOIN category c ON c.id = p.category_id ' . 'FROM product p JOIN category c ON c.id = p.category_id '
. 'LEFT JOIN product mv ON mv.id = p.maxi_variant_product_id '
. 'WHERE p.id = :id AND p.is_available = 1 AND c.is_active = 1 AND p.base_product_id IS NULL', . 'WHERE p.id = :id AND p.is_available = 1 AND c.is_active = 1 AND p.base_product_id IS NULL',
['id' => $id], ['id' => $id],
); );

View file

@ -200,7 +200,7 @@ class CatalogueController extends Controller
* variantes ; vide si le produit n'a pas de dimension taille. Chaque entree * variantes ; vide si le produit n'a pas de dimension taille. Chaque entree
* devient {product_id, size_cl, price_cents, label} ; le label humain est * devient {product_id, size_cl, price_cents, label} ; le label humain est
* derive du volume ("30 cl") -- aucun slug/enum ne fuit a l'ecran. * derive du volume ("30 cl") -- aucun slug/enum ne fuit a l'ecran.
* @return array{id: int, category_id: int, name: string, description: ?string, price_cents: int, image_path: ?string, display_order: int, sizes: list<array{product_id: int, size_cl: int, price_cents: int, label: string}>} * @return array{id: int, category_id: int, name: string, description: ?string, price_cents: int, image_path: ?string, display_order: int, maxi_variant_name: ?string, sizes: list<array{product_id: int, size_cl: int, price_cents: int, label: string}>}
*/ */
private function presentProduct(array $row, array $sizes = []): array private function presentProduct(array $row, array $sizes = []): array
{ {
@ -212,6 +212,10 @@ class CatalogueController extends Controller
'price_cents' => (int) ($row['price_cents'] ?? 0), 'price_cents' => (int) ($row['price_cents'] ?? 0),
'image_path' => $this->nullableString($row['image_path'] ?? null), 'image_path' => $this->nullableString($row['image_path'] ?? null),
'display_order' => (int) ($row['display_order'] ?? 0), 'display_order' => (int) ($row['display_order'] ?? 0),
// Nom de la variante Maxi de l'accompagnement (ex. "Grande Frite") ; NULL si
// le produit n'a pas de variante. La borne l'affiche en format Maxi pour ne
// pas montrer "Moyenne Frite" sur un menu agrandi.
'maxi_variant_name' => $this->nullableString($row['maxi_variant_name'] ?? null),
'sizes' => array_map( 'sizes' => array_map(
static function (array $size): array { static function (array $size): array {
$cl = (int) ($size['size_cl'] ?? 0); $cl = (int) ($size['size_cl'] ?? 0);

View file

@ -85,6 +85,9 @@ export function loadProducts() {
// borne ne montre un picker que si sizes a plus d'une entree. // borne ne montre un picker que si sizes a plus d'une entree.
bySlug[slug].push({ bySlug[slug].push({
id: p.id, nom: p.name, prix: p.price_cents, image: p.image_path, type: 'produit', id: p.id, nom: p.name, prix: p.price_cents, image: p.image_path, type: 'produit',
// maxiNom : nom de la variante Maxi (ex. "Grande Frite") quand le produit
// en a une, sinon null. Le composeur de menu l'affiche en format Maxi.
maxiNom: p.maxi_variant_name ?? null,
sizes: Array.isArray(p.sizes) ? p.sizes : [], sizes: Array.isArray(p.sizes) ? p.sizes : [],
}); });
} }

View file

@ -49,11 +49,14 @@ export function compositionLabels(c) {
: ''; : '';
out.push(`${c.burger.libelle}${opts}`); out.push(`${c.burger.libelle}${opts}`);
} }
// libelle fait foi : en Maxi l'accompagnement porte deja sa variante par nom
// ("Grande Frite"). Plus de suffixe " grande" -- il doublait le nom ("Grande Frite
// grande") et mentait pour la boisson (le menu Maxi ne l'agrandit pas).
if (c.accompagnement) { if (c.accompagnement) {
out.push(`${c.accompagnement.libelle}${c.accompagnement.taille === 'G' ? ' grande' : ''}`); out.push(c.accompagnement.libelle);
} }
if (c.boisson) { if (c.boisson) {
out.push(`${c.boisson.libelle}${c.boisson.taille === 'G' ? ' grande' : ''}`); out.push(c.boisson.libelle);
} }
if (c.sauce) { if (c.sauce) {
out.push(c.sauce.libelle); out.push(c.sauce.libelle);

View file

@ -141,8 +141,9 @@ function renderCart() {
/** /**
* Builds the composition breakdown HTML for a menu cart line. * Builds the composition breakdown HTML for a menu cart line.
* Renders burger (with personalisation options), accompagnement with taille, * Renders burger (with personalisation options), accompagnement, boisson, sauce,
* boisson with taille, sauce, and the supplement summary if applicable. * and the supplement summary if applicable. Le format Maxi se lit dans le libelle de
* l'accompagnement (variante "Grande ...") et la ligne de supplement, pas un suffixe.
* *
* @param {Object} item cart item with type === 'menu' and composition object * @param {Object} item cart item with type === 'menu' and composition object
* @returns {string} HTML string * @returns {string} HTML string
@ -160,11 +161,14 @@ function renderCompositionBlock(item) {
: ''; : '';
parts.push(`${escHtml(c.burger.libelle)}${burgerOpts}`); parts.push(`${escHtml(c.burger.libelle)}${burgerOpts}`);
} }
// libelle fait foi : en Maxi l'accompagnement porte deja sa variante par nom
// ("Grande Frite"). Plus de suffixe taille -- il doublait le nom ("Grande Frite
// grande") et "normale"/"grande" mentait pour la boisson (le Maxi ne l'agrandit pas).
if (c.accompagnement) { if (c.accompagnement) {
parts.push(`${escHtml(c.accompagnement.libelle)}${c.accompagnement.taille === 'G' ? ' grande' : ' normale'}`); parts.push(escHtml(c.accompagnement.libelle));
} }
if (c.boisson) { if (c.boisson) {
parts.push(`${escHtml(c.boisson.libelle)}${c.boisson.taille === 'G' ? ' grande' : ' normale'}`); parts.push(escHtml(c.boisson.libelle));
} }
if (c.sauce) { if (c.sauce) {
parts.push(escHtml(c.sauce.libelle)); parts.push(escHtml(c.sauce.libelle));

View file

@ -26,6 +26,20 @@ import { refreshCartBadge } from './nav.js';
/* slot_type de l'API -> champ de composition attendu par le rendu panier existant. */ /* slot_type de l'API -> champ de composition attendu par le rendu panier existant. */
const SLOT_FIELD = { side: 'accompagnement', drink: 'boisson', sauce: 'sauce' }; const SLOT_FIELD = { side: 'accompagnement', drink: 'boisson', sauce: 'sauce' };
/**
* Libelle a afficher pour une option selon le format. En Maxi ('M'), un
* accompagnement a une variante agrandie (maxiNom, ex. "Grande Frite") : c'est ce
* nom que le client doit voir au moment de CHOISIR, pas le "Moyenne Frite" de base.
* Sans maxiNom (ex. les boissons, que le menu Maxi n'agrandit pas) ou en Normal,
* on garde le nom de base. Pur.
* @param {Object} option produit borne {nom, maxiNom?}
* @param {'N'|'M'} size
* @returns {string}
*/
export function optionLabel(option, size) {
return (size === 'M' && option.maxiNom) ? option.maxiNom : option.nom;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Fonctions PURES (cible des tests, sans DOM ni fetch) */ /* Fonctions PURES (cible des tests, sans DOM ni fetch) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -89,9 +103,13 @@ export function buildMenuCartItem(menu, model, { size, selections }) {
if (!chosen) continue; // slot optionnel laisse "sans" if (!chosen) continue; // slot optionnel laisse "sans"
const field = SLOT_FIELD[slot.slotType]; const field = SLOT_FIELD[slot.slotType];
if (!field) continue; if (!field) continue;
// libelle PORTE le nom affiche : en Maxi, l'accompagnement prend sa variante
// ("Grande Frite") ; la boisson n'a pas de maxiNom (le menu Maxi ne l'agrandit
// pas) donc garde son nom de base. Plus de suffixe " grande" cote rendu.
const libelle = (isMaxi && chosen.maxiNom) ? chosen.maxiNom : chosen.nom;
composition[field] = field === 'sauce' composition[field] = field === 'sauce'
? { id: chosen.id, libelle: chosen.nom } ? { id: chosen.id, libelle: chosen.nom }
: { id: chosen.id, libelle: chosen.nom, taille }; : { id: chosen.id, libelle, taille };
} }
return { return {
@ -294,18 +312,23 @@ function renderSlotStep(body, footer, modal, state, slot) {
<span class="composer-card__name">Sans</span> <span class="composer-card__name">Sans</span>
</button> </button>
</li>` : ''} </li>` : ''}
${slot.options.map(o => ` ${slot.options.map(o => {
// En Maxi, l'accompagnement s'affiche sous sa variante agrandie
// ("Grande Frite") : le client choisit en connaissance de cause.
const label = optionLabel(o, state.size);
return `
<li> <li>
<button class="composer-card ${state.selections[slot.id] === o.id ? 'composer-card--selected' : ''}" <button class="composer-card ${state.selections[slot.id] === o.id ? 'composer-card--selected' : ''}"
type="button" data-pid="${o.id}" type="button" data-pid="${o.id}"
aria-pressed="${state.selections[slot.id] === o.id}" aria-pressed="${state.selections[slot.id] === o.id}"
aria-label="${escHtml(o.nom)}"> aria-label="${escHtml(label)}">
<img class="composer-card__image" src="${escHtml(o.image)}" alt="${escHtml(o.nom)}" <img class="composer-card__image" src="${escHtml(o.image)}" alt="${escHtml(label)}"
onerror="this.src='assets/images/ui/logo.png';"> onerror="this.src='assets/images/ui/logo.png';">
<span class="composer-card__name">${escHtml(o.nom)}</span> <span class="composer-card__name">${escHtml(label)}</span>
</button> </button>
</li> </li>
`).join('')} `;
}).join('')}
</ul> </ul>
`; `;
body.querySelectorAll('#slot-grid .composer-card').forEach(btn => { body.querySelectorAll('#slot-grid .composer-card').forEach(btn => {

View file

@ -102,6 +102,8 @@ final class CatalogueControllerTest extends TestCase
'id' => '12', 'category_id' => '3', 'name' => 'Cheeseburger', 'id' => '12', 'category_id' => '3', 'name' => 'Cheeseburger',
'description' => 'Pain, steak, cheddar', 'price_cents' => '890', 'description' => 'Pain, steak, cheddar', 'price_cents' => '890',
'vat_rate' => '100', 'image_path' => 'cheese.png', 'display_order' => '1', 'vat_rate' => '100', 'image_path' => 'cheese.png', 'display_order' => '1',
// LEFT JOIN variante Maxi : NULL pour un produit sans variante.
'maxi_variant_name' => null,
], ],
]; ];
@ -113,7 +115,7 @@ final class CatalogueControllerTest extends TestCase
$product = $payload['data'][0]; $product = $payload['data'][0];
self::assertSame( self::assertSame(
['id', 'category_id', 'name', 'description', 'price_cents', 'image_path', 'display_order', 'sizes'], ['id', 'category_id', 'name', 'description', 'price_cents', 'image_path', 'display_order', 'maxi_variant_name', 'sizes'],
array_keys($product), array_keys($product),
); );
self::assertSame(12, $product['id']); self::assertSame(12, $product['id']);
@ -121,9 +123,31 @@ final class CatalogueControllerTest extends TestCase
self::assertSame(890, $product['price_cents']); // chaine -> int self::assertSame(890, $product['price_cents']); // chaine -> int
self::assertArrayNotHasKey('vat_rate', $product); // fiscal interne, non expose self::assertArrayNotHasKey('vat_rate', $product); // fiscal interne, non expose
self::assertArrayNotHasKey('is_available', $product); // toujours dispo ici -> non expose self::assertArrayNotHasKey('is_available', $product); // toujours dispo ici -> non expose
self::assertNull($product['maxi_variant_name']); // pas de variante -> null
self::assertSame([], $product['sizes']); // produit mono-taille -> sizes vide self::assertSame([], $product['sizes']); // produit mono-taille -> sizes vide
} }
public function testProductsListExposesMaxiVariantName(): void
{
$db = new FakeCatalogueDatabase();
// "Moyenne Frite" (accompagnement) a une variante Maxi "Grande Frite" : le
// LEFT JOIN remonte mv.name AS maxi_variant_name, expose tel quel a la borne.
$db->productsRows = [
[
'id' => '23', 'category_id' => '4', 'name' => 'Moyenne Frite',
'description' => null, 'price_cents' => '250',
'image_path' => 'frite.png', 'display_order' => '1',
'maxi_variant_name' => 'Grande Frite',
],
];
$response = $this->controller($db, '/api/products')->products();
self::assertSame(200, $response->status());
$product = $this->decode($response->body())['data'][0];
self::assertSame('Grande Frite', $product['maxi_variant_name']);
}
public function testProductsListPresentsSizesArrayForDrinkWithVariants(): void public function testProductsListPresentsSizesArrayForDrinkWithVariants(): void
{ {
$db = new FakeCatalogueDatabase(); $db = new FakeCatalogueDatabase();
@ -205,6 +229,8 @@ final class CatalogueControllerTest extends TestCase
'id' => '12', 'category_id' => '3', 'name' => 'Cheeseburger', 'id' => '12', 'category_id' => '3', 'name' => 'Cheeseburger',
'description' => null, 'price_cents' => '890', 'vat_rate' => '100', 'description' => null, 'price_cents' => '890', 'vat_rate' => '100',
'image_path' => null, 'display_order' => '1', 'image_path' => null, 'display_order' => '1',
// Detail d'un accompagnement avec variante Maxi : le nom doit ressortir.
'maxi_variant_name' => 'Grande Frite',
]; ];
$response = $this->controller($db, '/api/products/12')->product(['id' => '12']); $response = $this->controller($db, '/api/products/12')->product(['id' => '12']);
@ -215,6 +241,7 @@ final class CatalogueControllerTest extends TestCase
self::assertSame(12, $product['id']); self::assertSame(12, $product['id']);
self::assertSame(890, $product['price_cents']); self::assertSame(890, $product['price_cents']);
self::assertNull($product['description']); self::assertNull($product['description']);
self::assertSame('Grande Frite', $product['maxi_variant_name']); // variante exposee
self::assertArrayNotHasKey('vat_rate', $product); self::assertArrayNotHasKey('vat_rate', $product);
// L'id a bien ete lie a la lecture, converti en entier (le repo a recu :id = 12). // L'id a bien ete lie a la lecture, converti en entier (le repo a recu :id = 12).
self::assertSame(12, $db->reads[0]['params']['id'] ?? null); self::assertSame(12, $db->reads[0]['params']['id'] ?? null);

View file

@ -9,14 +9,14 @@ import { test, before } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
let buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable; let buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable, optionLabel;
before(async () => { before(async () => {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://kiosk.test/product.html' }); const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { url: 'https://kiosk.test/product.html' });
global.window = dom.window; global.window = dom.window;
global.document = dom.window.document; global.document = dom.window.document;
global.localStorage = dom.window.localStorage; global.localStorage = dom.window.localStorage;
({ buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable } = ({ buildComposerSteps, buildMenuCartItem, selectionsComplete, composerIsViable, optionLabel } =
await import('../../src/public/borne/assets/js/page-product-menu.js')); await import('../../src/public/borne/assets/js/page-product-menu.js'));
}); });
@ -33,12 +33,14 @@ const detail = () => ({
}); });
const byId = () => ({ const byId = () => ({
100: { id: 100, nom: 'Le 280', prix: 0, image: 'b.png', type: 'produit' }, 100: { id: 100, nom: 'Le 280', prix: 0, image: 'b.png', type: 'produit', maxiNom: null },
22: { id: 22, nom: 'Frites', prix: 0, image: 'f.png', type: 'produit' }, // Accompagnements : variante Maxi (maxiNom) renseignee -> agrandissable.
23: { id: 23, nom: 'Potatoes', prix: 0, image: 'p.png', type: 'produit' }, 22: { id: 22, nom: 'Moyenne Frite', prix: 0, image: 'f.png', type: 'produit', maxiNom: 'Grande Frite' },
14: { id: 14, nom: 'Coca', prix: 0, image: 'c.png', type: 'produit' }, 23: { id: 23, nom: 'Potatoes', prix: 0, image: 'p.png', type: 'produit', maxiNom: 'Grande Potatoes' },
15: { id: 15, nom: 'Eau', prix: 0, image: 'e.png', type: 'produit' }, // Boissons : pas de variante Maxi (le menu Maxi n'agrandit pas la boisson).
47: { id: 47, nom: 'Ketchup', prix: 0, image: 'k.png', type: 'produit' }, 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' }; const menu = { id: 1, nom: 'Menu Le 280', image: 'b.png', type: 'menu' };
@ -70,7 +72,8 @@ test('buildMenuCartItem Normal: prix normal, pas de supplement, taille N, compos
assert.equal(item.prix_cents, 880); assert.equal(item.prix_cents, 880);
assert.equal(item.supplement_cents, 0); assert.equal(item.supplement_cents, 0);
assert.equal(item.composition.burger.libelle, 'Le 280'); assert.equal(item.composition.burger.libelle, 'Le 280');
assert.deepEqual(item.composition.accompagnement, { id: 22, libelle: 'Frites', taille: 'N' }); // 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.boisson, { id: 14, libelle: 'Coca', taille: 'N' });
assert.deepEqual(item.composition.sauce, { id: 47, libelle: 'Ketchup' }); assert.deepEqual(item.composition.sauce, { id: 47, libelle: 'Ketchup' });
}); });
@ -84,6 +87,31 @@ test('buildMenuCartItem Maxi: supplement = maxi - normal, taille G sur side/drin
assert.equal(item.composition.boisson.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 (le Maxi ne l agrandit pas).
assert.equal(item.composition.boisson.libelle, 'Coca');
});
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', () => { test('buildMenuCartItem: slot optionnel non choisi -> champ absent de composition', () => {
const m = buildComposerSteps(detail(), byId()); const m = buildComposerSteps(detail(), byId());
const item = buildMenuCartItem(menu, m, { size: 'N', selections: { 1: 14, 16: 22 } }); // pas de sauce const item = buildMenuCartItem(menu, m, { size: 'N', selections: { 1: 14, 16: 22 } }); // pas de sauce

View file

@ -68,10 +68,20 @@ test('loadProducts groupe les produits par slug a la forme borne (type produit)'
const data = await loadProducts(); const data = await loadProducts();
assert.deepEqual(data.burgers, [ assert.deepEqual(data.burgers, [
// sizes (R4) : tableau vide par defaut quand l'API n'en renvoie pas. // sizes (R4) : tableau vide par defaut quand l'API n'en renvoie pas.
{ id: 10, nom: 'Big Mac', prix: 600, image: 'assets/images/produits/burgers/bigmac.png', type: 'produit', sizes: [] }, // maxiNom : null par defaut quand l'API n'envoie pas maxi_variant_name.
{ id: 10, nom: 'Big Mac', prix: 600, image: 'assets/images/produits/burgers/bigmac.png', type: 'produit', maxiNom: null, sizes: [] },
]); ]);
}); });
test('loadProducts reporte maxi_variant_name -> maxiNom (variante Maxi de l accompagnement)', async () => {
const fx = fixtures();
fx['/api/products'].data[0].maxi_variant_name = 'Grande Frite';
const { loadProducts } = await freshData(fx);
const data = await loadProducts();
assert.equal(data.burgers[0].maxiNom, 'Grande Frite');
});
test('loadProducts reporte le tableau sizes du produit (R4) tel quel', async () => { test('loadProducts reporte le tableau sizes du produit (R4) tel quel', async () => {
const fx = fixtures(); const fx = fixtures();
fx['/api/products'].data[0].sizes = [ fx['/api/products'].data[0].sizes = [

View file

@ -40,8 +40,10 @@ const menu = (over = {}) => ({
supplement_cents: 50, image: 'm.png', supplement_cents: 50, image: 'm.png',
composition: { composition: {
burger: { libelle: 'Big Mac', options: ['sans-oignon', 'avec-fromage'] }, burger: { libelle: 'Big Mac', options: ['sans-oignon', 'avec-fromage'] },
accompagnement: { libelle: 'Frites', taille: 'G' }, // Maxi : l accompagnement porte deja sa variante par NOM (le serveur substitue
boisson: { libelle: 'Coca', taille: 'M' }, // Moyenne -> Grande). Le libelle fait foi, plus de suffixe " grande".
accompagnement: { libelle: 'Grande Frite', taille: 'G' },
boisson: { libelle: 'Coca', taille: 'G' },
sauce: { libelle: 'Ketchup' }, sauce: { libelle: 'Ketchup' },
}, },
...over, ...over,
@ -63,14 +65,19 @@ test('compositionLabels: undefined -> []', () => {
assert.deepEqual(compositionLabels(undefined), []); assert.deepEqual(compositionLabels(undefined), []);
}); });
test('compositionLabels: liste burger(options)/accompagnement(taille)/boisson/sauce', () => { test('compositionLabels: libelle fait foi, le suffixe " grande" trompeur est supprime', () => {
const labels = compositionLabels(menu().composition); const labels = compositionLabels(menu().composition);
assert.deepEqual(labels, [ assert.deepEqual(labels, [
'Big Mac (sans oignon, avec fromage)', 'Big Mac (sans oignon, avec fromage)',
'Frites grande', 'Grande Frite', // variante par nom, plus de "Moyenne Frite grande"
'Coca', 'Coca', // boisson non agrandie : pas de faux " grande"
'Ketchup', 'Ketchup',
]); ]);
// Garde-fou explicite contre la regression du bug rapporte.
const sideLabel = labels[1];
assert.equal(sideLabel.includes('Moyenne Frite grande'), false);
assert.equal(sideLabel.endsWith(' grande'), false);
assert.ok(sideLabel.includes('Grande Frite'));
}); });
test('compositionLabels: composants absents ignores sans jeter', () => { test('compositionLabels: composants absents ignores sans jeter', () => {