corentin_wakdo/tests/js/data.test.js
Corentin JOGUET 545aa19cf1
All checks were successful
CI / secret-scan (push) Successful in 18s
CI / php-lint (push) Successful in 33s
CI / static-tests (push) Successful in 1m9s
CI / js-tests (push) Successful in 41s
feat(borne): tailles 30/50cl boissons a la carte (R4) (#88)
2026-06-22 14:07:46 +02:00

205 lines
8.9 KiB
JavaScript

/*
* Tests de la couche data.js du front borne (node:test, sans DOM).
*
* Couvre le swap P4 : data.js consomme l'API REST (/api/categories|products|menus),
* deballe l'enveloppe {data}, et traduit la forme canonique (snake_case, name,
* price_cents, image_path) vers la forme attendue par la borne (nom, prix, image,
* type, objet indexe par slug, menus sous la cle 'menus'). fetch est mocke ; chaque
* cas reimporte data.js avec une query unique pour repartir d'un cache vide.
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
let _seq = 0;
/**
* Installe un mock de fetch route par URL et reimporte data.js avec un cache neuf.
* @param {Record<string, unknown>} routes reponses JSON par URL
* @param {string[]} [calls] collecteur des URLs appelees (optionnel)
*/
async function freshData(routes, calls) {
global.fetch = async (url) => {
if (calls) calls.push(url);
if (!(url in routes)) throw new Error(`fetch inattendu: ${url}`);
return { ok: true, status: 200, json: async () => routes[url] };
};
return import(`../../src/public/borne/assets/js/data.js?case=${_seq++}`);
}
function fixtures() {
return {
'/api/categories': {
data: [
{ id: 1, name: 'Menus', slug: 'menus', image_path: 'assets/images/categories/menus.png', display_order: 1 },
{ id: 3, name: 'Burgers', slug: 'burgers', image_path: 'assets/images/categories/burgers.png', display_order: 3 },
],
total: 2,
},
'/api/products': {
data: [
{ id: 10, category_id: 3, name: 'Big Mac', description: 'Pain, steak, cheddar', price_cents: 600, image_path: 'assets/images/produits/burgers/bigmac.png', display_order: 4 },
],
total: 1,
},
'/api/menus': {
data: [
{ id: 1, category_id: 1, burger_product_id: 10, name: 'Menu Big Mac', description: null, price_normal_cents: 800, price_maxi_cents: 950, image_path: 'assets/images/produits/burgers/bigmac.png', display_order: 1 },
],
total: 1,
},
};
}
test('loadCategories appelle /api/categories, deballe {data} et mappe name->title, image_path->image', async () => {
const calls = [];
const { loadCategories } = await freshData(fixtures(), calls);
const cats = await loadCategories();
assert.ok(Array.isArray(cats));
assert.equal(cats.length, 2);
assert.deepEqual(cats[0], { id: 1, title: 'Menus', slug: 'menus', image: 'assets/images/categories/menus.png' });
assert.ok(calls.includes('/api/categories'), 'doit fetch /api/categories');
});
test('loadProducts groupe les produits par slug a la forme borne (type produit)', async () => {
const { loadProducts } = await freshData(fixtures());
const data = await loadProducts();
assert.deepEqual(data.burgers, [
// 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: [] },
]);
});
test('loadProducts reporte le tableau sizes du produit (R4) tel quel', async () => {
const fx = fixtures();
fx['/api/products'].data[0].sizes = [
{ product_id: 10, size_cl: 30, price_cents: 600, label: '30 cl' },
{ product_id: 88, size_cl: 50, price_cents: 650, label: '50 cl' },
];
const { loadProducts } = await freshData(fx);
const data = await loadProducts();
assert.equal(data.burgers[0].sizes.length, 2);
assert.equal(data.burgers[0].sizes[1].product_id, 88);
});
test('loadProducts glisse les menus sous la cle menus (type menu, prix = price_normal_cents)', async () => {
const { loadProducts } = await freshData(fixtures());
const data = await loadProducts();
assert.deepEqual(data.menus, [
{ id: 1, nom: 'Menu Big Mac', prix: 800, image: 'assets/images/produits/burgers/bigmac.png', type: 'menu' },
]);
});
test('loadProducts consomme bien les trois endpoints /api/*', async () => {
const calls = [];
const { loadProducts } = await freshData(fixtures(), calls);
await loadProducts();
for (const url of ['/api/categories', '/api/products', '/api/menus']) {
assert.ok(calls.includes(url), `doit fetch ${url}`);
}
});
test('getProductsByCategory derive de loadProducts (forme borne), [] si slug inconnu', async () => {
const { getProductsByCategory } = await freshData(fixtures());
const burgers = await getProductsByCategory('burgers');
assert.equal(burgers.length, 1);
assert.equal(burgers[0].nom, 'Big Mac');
assert.deepEqual(await getProductsByCategory('inexistant'), []);
});
test('findProduct trouve un produit et l enrichit de sa categorie (slug)', async () => {
const { findProduct } = await freshData(fixtures());
const product = await findProduct(10);
assert.equal(product.nom, 'Big Mac');
assert.equal(product.prix, 600);
assert.equal(product.type, 'produit');
assert.equal(product.categorie, 'burgers');
});
test('findProduct trouve un menu (type menu, categorie menus, prix normal)', async () => {
const { findProduct } = await freshData(fixtures());
const menu = await findProduct(1);
assert.equal(menu.nom, 'Menu Big Mac');
assert.equal(menu.type, 'menu');
assert.equal(menu.categorie, 'menus');
assert.equal(menu.prix, 800);
});
test('un statut HTTP non-ok fait rejeter le chargement', async () => {
global.fetch = async () => ({ ok: false, status: 500, json: async () => ({}) });
const { loadCategories } = await import(`../../src/public/borne/assets/js/data.js?case=err${_seq++}`);
await assert.rejects(() => loadCategories());
});
test('findProduct desambigue par categorie quand un produit et un menu partagent un id', async () => {
// product et menu sont deux tables auto-increment distinctes : l'id 4 designe a
// la fois le burger Big Mac (product) et le Menu Big Mac (menu). Sans categorie,
// un scan global renverrait le menu (scanne avant burgers) -> mauvais produit.
const colliding = {
'/api/categories': {
data: [
{ id: 1, name: 'Menus', slug: 'menus', image_path: 'm.png', display_order: 1 },
{ id: 3, name: 'Burgers', slug: 'burgers', image_path: 'b.png', display_order: 3 },
],
},
'/api/products': {
data: [
{ id: 4, category_id: 3, name: 'Big Mac', description: null, price_cents: 600, image_path: 'bigmac.png', display_order: 4 },
],
},
'/api/menus': {
data: [
{ id: 4, category_id: 1, burger_product_id: 4, name: 'Menu Big Mac', description: null, price_normal_cents: 800, price_maxi_cents: 950, image_path: 'bigmac.png', display_order: 1 },
],
},
};
const { findProduct } = await freshData(colliding);
const burger = await findProduct(4, 'burgers');
assert.equal(burger.type, 'produit', 'la categorie burgers doit donner le produit, pas le menu');
assert.equal(burger.nom, 'Big Mac');
assert.equal(burger.categorie, 'burgers');
const menu = await findProduct(4, 'menus');
assert.equal(menu.type, 'menu');
assert.equal(menu.nom, 'Menu Big Mac');
assert.equal(menu.categorie, 'menus');
});
test('findProduct renvoie null si l id est absent de la categorie ciblee', async () => {
const { findProduct } = await freshData(fixtures());
assert.equal(await findProduct(999, 'burgers'), null);
});
test('loadProductsById indexe les PRODUITS par id et exclut les menus (anti-collision)', async () => {
// colliding : un produit id 4 ET un menu id 4. L'index ne doit contenir QUE le
// produit a la cle 4 (les option_product_ids des slots referencent des produits).
const colliding = {
'/api/categories': { data: [
{ id: 1, name: 'Menus', slug: 'menus', image_path: 'm.png', display_order: 1 },
{ id: 3, name: 'Burgers', slug: 'burgers', image_path: 'b.png', display_order: 3 },
] },
'/api/products': { data: [
{ id: 4, category_id: 3, name: 'Big Mac', description: null, price_cents: 600, image_path: 'bigmac.png', display_order: 4 },
{ id: 14, category_id: 3, name: 'Coca', description: null, price_cents: 190, image_path: 'coca.png', display_order: 1 },
] },
'/api/menus': { data: [
{ id: 4, category_id: 1, burger_product_id: 4, name: 'Menu Big Mac', description: null, price_normal_cents: 800, price_maxi_cents: 950, image_path: 'bigmac.png', display_order: 1 },
] },
};
const { loadProductsById } = await freshData(colliding);
const byId = await loadProductsById();
assert.equal(byId[4].type, 'produit', 'id 4 doit etre le PRODUIT, pas le menu');
assert.equal(byId[4].nom, 'Big Mac');
assert.equal(byId[14].nom, 'Coca');
assert.equal(Object.keys(byId).length, 2, 'que les 2 produits, le menu est exclu');
});