corentin_wakdo/tests/js/allergens.test.js
Imugiii eb2891238f
All checks were successful
CI / secret-scan (pull_request) Successful in 7s
CI / php-lint (pull_request) Successful in 19s
CI / static-tests (pull_request) Successful in 39s
CI / js-tests (pull_request) Successful in 19s
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 18s
CI / static-tests (push) Successful in 39s
CI / js-tests (push) Successful in 16s
CI / auto-merge (pull_request) Successful in 4s
CI / auto-merge (push) Has been skipped
feat(borne): modale allergenes generale (14 INCO) sur carte et fiche + harnais tests JS (P3)
PR-C du lot P3. Icone "i" allergenes sur la borne ouvrant une modale GENERALE
listant les 14 allergenes a declaration obligatoire (UE INCO 1169/2011). Info
generale, PAS un calcul par produit (mapping ingredient_allergen reste differe).

- data/allergens.json : les 14 INCO (liste fixe data borne). loadAllergens()
  ajoute a data.js avec point de swap P4 documente (-> /api/allergens).
- allergens.js : module CSP-safe (DOM API, aucun handler inline). buildAllergenInfoButton
  (bouton "i", stopPropagation sur la carte), openAllergenModal (idempotent),
  fermeture par bouton / clic overlay / touche Echap.
- Integration : carte produit (page-products.js) ET fiche produit simple
  (page-product.js) ; CSS modale + badge reutilisant le pattern overlay du composer.

Harnais de tests front (premier du depot) : node:test + jsdom. tests/js/allergens.test.js
couvre les 14 INCO, la construction du bouton, l'ouverture/listing/fermeture et
l'idempotence (7 tests). Scoping ESM local (src/public/borne/assets/js + tests/js)
pour garder la racine en CommonJS (hooks, _byan, bin inchanges). Job CI js-tests
(Node 20 epingle) ajoute aux checks requis de l'auto-merge.
2026-06-17 10:09:31 +00:00

110 lines
4.1 KiB
JavaScript

/*
* Tests du module allergens du front borne (node:test + jsdom).
*
* Couvre le contrat de PR-C : la liste fixe des 14 allergenes INCO (data borne,
* se branchera sur /api/allergens au swap P4), la construction du bouton "i", et
* la modale GENERALE (ouverture, listing des 14, fermeture par bouton/overlay/
* Escape, idempotence). DOM simule par jsdom : aucun navigateur requis.
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { JSDOM } from 'jsdom';
import {
buildAllergenInfoButton,
openAllergenModal,
closeAllergenModal,
} from '../../src/public/borne/assets/js/allergens.js';
const here = dirname(fileURLToPath(import.meta.url));
const allergensJsonPath = join(here, '../../src/public/borne/data/allergens.json');
function setupDom() {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
global.window = dom.window;
global.document = dom.window.document;
return dom;
}
function loadAllergensFixture() {
return JSON.parse(readFileSync(allergensJsonPath, 'utf8'));
}
test('data/allergens.json liste exactement les 14 allergenes INCO', () => {
const list = loadAllergensFixture();
assert.ok(Array.isArray(list));
assert.equal(list.length, 14);
for (const a of list) {
assert.equal(typeof a.id, 'number');
assert.equal(typeof a.name, 'string');
assert.ok(a.name.trim().length > 0);
}
const names = list.map((a) => a.name);
assert.equal(new Set(names).size, 14, 'noms uniques');
// Quelques jalons de la liste reglementaire (UE INCO 1169/2011 annexe II).
const joined = names.join(' | ').toLowerCase();
for (const expected of ['gluten', 'lait', 'arachide', 'soja', 'mollusque']) {
assert.ok(joined.includes(expected), `attendu: ${expected}`);
}
});
test('buildAllergenInfoButton cree un bouton "i" qui declenche onOpen', () => {
setupDom();
let opened = 0;
const btn = buildAllergenInfoButton(() => { opened += 1; });
assert.equal(btn.tagName, 'BUTTON');
assert.equal(btn.type, 'button');
assert.ok(btn.className.includes('allergen-info-btn'));
assert.ok(btn.getAttribute('aria-label'));
btn.click();
assert.equal(opened, 1, 'le clic ouvre la modale');
});
test('openAllergenModal affiche une modale listant les 14 allergenes', () => {
setupDom();
const list = loadAllergensFixture();
const overlay = openAllergenModal(list);
assert.ok(document.body.contains(overlay));
assert.equal(overlay.getAttribute('role'), 'dialog');
assert.equal(overlay.getAttribute('aria-modal'), 'true');
const items = overlay.querySelectorAll('.allergen-modal-list li');
assert.equal(items.length, 14);
assert.ok(overlay.textContent.toLowerCase().includes('lait'));
});
test('la modale se ferme via le bouton de fermeture', () => {
setupDom();
openAllergenModal(loadAllergensFixture());
document.querySelector('.allergen-modal-close').click();
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
});
test('la modale se ferme par clic sur l overlay (hors contenu)', () => {
const dom = setupDom();
const overlay = openAllergenModal(loadAllergensFixture());
overlay.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
});
test('la modale se ferme avec la touche Echap', () => {
const dom = setupDom();
openAllergenModal(loadAllergensFixture());
document.dispatchEvent(new dom.window.KeyboardEvent('keydown', { key: 'Escape' }));
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
});
test('ouvrir deux fois ne duplique pas la modale (idempotent)', () => {
setupDom();
const list = loadAllergensFixture();
openAllergenModal(list);
openAllergenModal(list);
assert.equal(document.querySelectorAll('.allergen-modal-overlay').length, 1);
closeAllergenModal();
assert.equal(document.querySelector('.allergen-modal-overlay'), null);
});