release: dev -> main v0.2.0 #93

Merged
Corentin merged 96 commits from dev into main 2026-06-23 10:09:58 +02:00
4 changed files with 112 additions and 11 deletions
Showing only changes of commit 042c30a5fe - Show all commits

View file

@ -6,12 +6,38 @@
* element with [data-mode-badge] on the page.
* - Sync the cart item count into any element with [data-cart-count].
* - Handle the mode query-string on page load (welcome -> categories handoff).
* - Guard : une page au-dela de l'accueil EXIGE un mode de consommation. Sans
* mode (ex. localStorage vide en cours de session), la borne POSTerait
* service_mode:null et la commande est rejetee en 422. Sur une page profonde
* sans mode, on renvoie vers l'accueil pour que le mode soit (re)choisi.
*
* Import this module in every page that has a header.
*/
import { getMode, setMode, getCartCount } from './state.js';
const VALID_MODES = ['sur-place', 'a-emporter'];
/** Libelle humain d'un mode ; chaine vide si aucun mode valide (ne ment pas). */
export function modeLabel(mode) {
if (mode === 'a-emporter') return 'A emporter';
if (mode === 'sur-place') return 'Sur place';
return '';
}
/**
* Faut-il renvoyer vers l'accueil ? Vrai hors de l'ecran d'accueil quand aucun
* mode de consommation valide n'est memorise. Pur (teste sans DOM). Sans cette
* garde, atteindre une page de commande sans mode mene a service_mode:null -> 422.
* @param {string} pathname window.location.pathname
* @param {string|null} mode mode memorise
* @returns {boolean}
*/
export function needsModeRedirect(pathname, mode) {
const onWelcome = pathname === '/' || pathname.endsWith('/index.html');
return !onWelcome && !VALID_MODES.includes(mode);
}
/**
* Reads ?mode= from the current URL and persists it if present.
* Called once on DOMContentLoaded so that the welcome -> categories
@ -20,7 +46,7 @@ import { getMode, setMode, getCartCount } from './state.js';
function syncModeFromURL() {
const params = new URLSearchParams(window.location.search);
const modeParam = params.get('mode');
if (modeParam === 'sur-place' || modeParam === 'a-emporter') {
if (VALID_MODES.includes(modeParam)) {
setMode(modeParam);
}
}
@ -29,8 +55,7 @@ function syncModeFromURL() {
* Renders the human-readable mode label into every [data-mode-badge] element.
*/
function renderModeBadge() {
const mode = getMode();
const label = mode === 'a-emporter' ? 'A emporter' : 'Sur place';
const label = modeLabel(getMode());
document.querySelectorAll('[data-mode-badge]').forEach(el => {
el.textContent = label;
});
@ -48,9 +73,17 @@ export function refreshCartBadge() {
});
}
/* Initialise on DOM ready */
document.addEventListener('DOMContentLoaded', () => {
syncModeFromURL();
renderModeBadge();
refreshCartBadge();
});
/* Initialise on DOM ready. Garde derriere typeof document pour rester importable
* en test pur (node sans jsdom) : modeLabel/needsModeRedirect n'ont alors aucun effet de bord. */
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
syncModeFromURL();
// Mode absent sur une page profonde -> retour accueil (evite le 422 service_mode:null).
if (needsModeRedirect(window.location.pathname, getMode())) {
window.location.replace('index.html');
return;
}
renderModeBadge();
refreshCartBadge();
});
}

View file

@ -60,8 +60,17 @@ async function doSubmit(serviceTag) {
function startCheckout() {
if (checkingOut) return;
const mode = getMode();
// Garde finale (defense en profondeur, en plus de la garde nav.js) : sans mode de
// consommation valide (ex. localStorage vide), ne PAS soumettre une commande a
// service_mode null (rejetee en 422 INVALID_SERVICE_MODE). On renvoie a l'accueil
// pour (re)choisir le mode, le panier etant conserve.
if (mode !== 'sur-place' && mode !== 'a-emporter') {
window.location.replace('index.html');
return;
}
checkingOut = true;
if (getMode() === 'sur-place') {
if (mode === 'sur-place') {
openChevalet(tag => doSubmit(tag), () => { checkingOut = false; });
} else {
doSubmit('');
@ -176,7 +185,8 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
if (recap) {
const modeLabel = getMode() === 'a-emporter' ? 'A emporter' : 'Sur place';
const m = getMode();
const modeLabel = m === 'a-emporter' ? 'A emporter' : (m === 'sur-place' ? 'Sur place' : '');
recap.innerHTML = `
<p class="payment-recap__mode">${escHtml(modeLabel)}</p>
<p class="payment-recap__items">${items.length} article${items.length > 1 ? 's' : ''}</p>

View file

@ -177,6 +177,11 @@
</nav>
</main>
<!-- nav.js : persiste le mode de consommation recu en ?mode= depuis l'accueil
(syncModeFromURL) AVANT que l'utilisateur ne suive une carte vers products.html.
Sans ce script, le mode n'atteint jamais localStorage et le paiement part en
service_mode null (422). C'est la 1re page post-accueil, elle doit donc le charger. -->
<script type="module" src="assets/js/nav.js"></script>
<script type="module" src="assets/js/a11y.js"></script>
</body>
</html>

53
tests/js/nav.test.js Normal file
View file

@ -0,0 +1,53 @@
/*
* nav.test.js Garde du mode de consommation borne (helpers PURS de nav.js).
*
* nav.js n'enregistre son listener DOMContentLoaded que derriere `typeof document`,
* donc l'import est sans effet de bord en node pur : on teste needsModeRedirect et
* modeLabel sans jsdom. Couvre la cause du 422 INVALID_SERVICE_MODE : atteindre une
* page de commande sans mode memorise (localStorage vide) doit renvoyer a l'accueil,
* et le badge ne doit jamais afficher un faux "Sur place" quand aucun mode n'est choisi.
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { needsModeRedirect, modeLabel } from '../../src/public/borne/assets/js/nav.js';
/* --- modeLabel ----------------------------------------------------------- */
test('modeLabel: libelle humain ; vide si mode absent ou inconnu (ne ment pas)', () => {
assert.equal(modeLabel('sur-place'), 'Sur place');
assert.equal(modeLabel('a-emporter'), 'A emporter');
assert.equal(modeLabel(null), '');
assert.equal(modeLabel(undefined), '');
assert.equal(modeLabel('bidon'), '');
});
/* --- needsModeRedirect --------------------------------------------------- */
test('needsModeRedirect: page profonde SANS mode valide -> redirige', () => {
assert.equal(needsModeRedirect('/payment.html', null), true);
assert.equal(needsModeRedirect('/products.html', undefined), true);
assert.equal(needsModeRedirect('/cart.html', 'bidon'), true);
assert.equal(needsModeRedirect('/categories.html', ''), true);
});
test('needsModeRedirect: mode valide -> pas de redirection', () => {
assert.equal(needsModeRedirect('/payment.html', 'sur-place'), false);
assert.equal(needsModeRedirect('/products.html', 'a-emporter'), false);
});
test('needsModeRedirect: ecran d accueil jamais redirige (le mode s y choisit)', () => {
assert.equal(needsModeRedirect('/', null), false);
assert.equal(needsModeRedirect('/index.html', null), false);
assert.equal(needsModeRedirect('/index.html', 'bidon'), false);
});
/* --- Cablage de la chaine de persistance (regression) -------------------- */
test('categories.html charge nav.js (sinon le mode recu en ?mode= n est jamais persiste)', () => {
// 1re page post-accueil : elle recoit ?mode= depuis index.html et DOIT le persister
// via syncModeFromURL. Sans nav.js ici, le mode n atteint pas localStorage et la
// garde renverrait en boucle un utilisateur legitime vers l accueil (regression revue).
const html = readFileSync(new URL('../../src/public/borne/categories.html', import.meta.url), 'utf8');
assert.match(html, /assets\/js\/nav\.js/);
});