corentin_wakdo/tests/js/checkout.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

127 lines
5.9 KiB
JavaScript

/*
* Tests de checkout.js (P5 L4), node:test. checkout.js + state.js + data.js n'ont
* aucun acces DOM au chargement -> import statique. Cible : traduction PURE
* panier->contrat /api/orders (mapServiceMode, buildSelections, buildOrderItem,
* buildOrderPayload) + submitOrder (fetch + localStorage mockes).
*/
import { test, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import {
mapServiceMode, buildSelections, buildOrderItem, buildOrderPayload, submitOrder,
} from '../../src/public/borne/assets/js/checkout.js';
/* --- mapServiceMode ------------------------------------------------------ */
test('mapServiceMode: borne -> contrat (dine_in / takeaway), inconnu -> null', () => {
assert.equal(mapServiceMode('sur-place'), 'dine_in');
assert.equal(mapServiceMode('a-emporter'), 'takeaway');
assert.equal(mapServiceMode(null), null);
});
/* --- buildSelections ----------------------------------------------------- */
const slots = () => ([
{ id: 1, option_product_ids: [14, 15] }, // boisson
{ id: 16, option_product_ids: [22, 23] }, // accompagnement
{ id: 31, option_product_ids: [47] }, // sauce
]);
test('buildSelections: mappe les produits choisis a leur slot', () => {
const comp = { accompagnement: { id: 22 }, boisson: { id: 14 }, sauce: { id: 47 } };
assert.deepEqual(buildSelections(comp, slots()), [
{ menu_slot_id: 16, product_id: 22 },
{ menu_slot_id: 1, product_id: 14 },
{ menu_slot_id: 31, product_id: 47 },
]);
});
test('buildSelections: produit hors slots ignore ; composition vide -> []', () => {
assert.deepEqual(buildSelections({ boisson: { id: 999 } }, slots()), []);
assert.deepEqual(buildSelections(undefined, slots()), []);
});
/* --- buildOrderItem ------------------------------------------------------ */
test('buildOrderItem: produit simple', () => {
assert.deepEqual(buildOrderItem({ id: 14, type: 'produit', quantite: 3 }, {}), {
type: 'product', product_id: 14, quantity: 3,
});
});
test('buildOrderItem: menu normal vs maxi (format + selections)', () => {
const menuItem = { id: 1, type: 'menu', quantite: 1, supplement_cents: 0,
composition: { accompagnement: { id: 22 }, boisson: { id: 14 } } };
const normal = buildOrderItem(menuItem, { 1: slots() });
assert.equal(normal.format, 'normal');
assert.equal(normal.menu_id, 1);
assert.equal(normal.selections.length, 2);
const maxi = buildOrderItem({ ...menuItem, supplement_cents: 150 }, { 1: slots() });
assert.equal(maxi.format, 'maxi');
});
test('buildOrderItem: format explicite prime sur l inference (maxi meme si supplement 0)', () => {
// Le choix utilisateur est transporte dans cartItem.format ; il ne doit PAS etre
// re-devine du prix (un menu maxi == normal serait sinon envoye en normal).
const explicit = { id: 1, type: 'menu', quantite: 1, supplement_cents: 0, format: 'maxi',
composition: { boisson: { id: 14 } } };
assert.equal(buildOrderItem(explicit, { 1: slots() }).format, 'maxi');
// Repli historique : un panier serialise sans champ format infere depuis supplement.
const legacy = { id: 1, type: 'menu', quantite: 1, supplement_cents: 150, composition: {} };
assert.equal(buildOrderItem(legacy, { 1: slots() }).format, 'maxi');
});
/* --- buildOrderPayload --------------------------------------------------- */
test('buildOrderPayload: dine_in inclut service_tag ; takeaway l omet', () => {
const cart = [{ id: 14, type: 'produit', quantite: 1 }];
const din = buildOrderPayload(cart, 'sur-place', '261', {}, 'key-1');
assert.equal(din.service_mode, 'dine_in');
assert.equal(din.service_tag, '261');
assert.equal(din.idempotency_key, 'key-1');
assert.equal(din.items.length, 1);
const take = buildOrderPayload(cart, 'a-emporter', '', {}, 'key-2');
assert.equal(take.service_mode, 'takeaway');
assert.equal('service_tag' in take, false);
});
/* --- submitOrder (mocks) ------------------------------------------------- */
function stubEnv(cart, mode) {
const store = { wakdo_cart: JSON.stringify(cart), wakdo_mode: mode };
global.localStorage = {
getItem: (k) => (k in store ? store[k] : null),
setItem: (k, v) => { store[k] = String(v); },
removeItem: (k) => { delete store[k]; },
};
}
test('submitOrder: re-fetch slots, POST create puis pay, renvoie order_number', async () => {
stubEnv([{ id: 1, type: 'menu', quantite: 1, supplement_cents: 0, composition: { boisson: { id: 14 }, accompagnement: { id: 22 } } }], 'sur-place');
const calls = [];
global.fetch = async (url, opts) => {
calls.push({ url, method: opts?.method, body: opts?.body ? JSON.parse(opts.body) : null });
if (url === '/api/menus/1') return { ok: true, json: async () => ({ data: { slots: slots() } }) };
if (url === '/api/orders') return { ok: true, json: async () => ({ data: { order_number: 'K12', total_ttc_cents: 800, status: 'pending_payment' } }) };
if (url === '/api/orders/K12/pay') return { ok: true, json: async () => ({ data: { order_number: 'K12', total_ttc_cents: 800, status: 'paid' } }) };
throw new Error(`URL inattendue: ${url}`);
};
const res = await submitOrder({ serviceTag: '261' });
assert.equal(res.order_number, 'K12');
assert.equal(res.total_ttc_cents, 800);
const create = calls.find(c => c.url === '/api/orders');
assert.equal(create.method, 'POST');
assert.equal(create.body.service_mode, 'dine_in');
assert.equal(create.body.service_tag, '261');
assert.equal(create.body.items[0].type, 'menu');
assert.equal(create.body.items[0].selections.length, 2);
assert.ok(calls.some(c => c.url === '/api/orders/K12/pay' && c.method === 'POST'));
});
test('submitOrder: panier vide -> jette EMPTY_CART', async () => {
stubEnv([], 'a-emporter');
await assert.rejects(() => submitOrder(), /EMPTY_CART/);
});