227 lines
9.8 KiB
JavaScript
227 lines
9.8 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.
|
|
// maxiNom : null par defaut quand l'API n'envoie pas maxi_variant_name.
|
|
// commandable : true par defaut quand l'API n'envoie pas is_orderable.
|
|
{ id: 10, nom: 'Big Mac', prix: 600, image: 'assets/images/produits/burgers/bigmac.png', type: 'produit', maxiNom: null, sizes: [], commandable: true },
|
|
]);
|
|
});
|
|
|
|
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 () => {
|
|
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', commandable: true },
|
|
]);
|
|
});
|
|
|
|
test('loadProducts: is_orderable=false -> commandable=false (rupture RG-T21)', async () => {
|
|
const fx = fixtures();
|
|
fx['/api/products'].data[0].is_orderable = false;
|
|
fx['/api/menus'].data[0].is_orderable = false;
|
|
const { loadProducts } = await freshData(fx);
|
|
|
|
const data = await loadProducts();
|
|
assert.equal(data.burgers[0].commandable, false);
|
|
assert.equal(data.menus[0].commandable, false);
|
|
});
|
|
|
|
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');
|
|
});
|