fix(borne): persiste et garde le mode de consommation (corrige 422 INVALID_SERVICE_MODE au paiement)
All checks were successful
CI / secret-scan (pull_request) Successful in 10s
CI / php-lint (pull_request) Successful in 24s
CI / static-tests (pull_request) Successful in 48s
CI / js-tests (pull_request) Successful in 30s
CI / php-lint (push) Successful in 26s
CI / static-tests (push) Successful in 1m11s
CI / js-tests (push) Successful in 34s
CI / secret-scan (push) Successful in 11s
All checks were successful
CI / secret-scan (pull_request) Successful in 10s
CI / php-lint (pull_request) Successful in 24s
CI / static-tests (pull_request) Successful in 48s
CI / js-tests (pull_request) Successful in 30s
CI / php-lint (push) Successful in 26s
CI / static-tests (push) Successful in 1m11s
CI / js-tests (push) Successful in 34s
CI / secret-scan (push) Successful in 11s
Le paiement borne echouait en POST /api/orders -> 422 INVALID_SERVICE_MODE car le mode de consommation (sur place / a emporter) n'atteignait jamais localStorage : la 1re page post-accueil (categories.html) ne chargeait pas nav.js, donc le ?mode= recu de l'accueil n'etait pas persiste, et ses cartes statiques pointaient vers products.html sans &mode=. Au paiement, mapServiceMode(null) -> service_mode null -> rejet serveur ; le badge affichait en plus un faux 'Sur place' qui masquait l'absence de mode. - categories.html : charge nav.js -> syncModeFromURL persiste le mode des l'etape categories (cause racine du 422 ; sans ca la garde ci-dessous bouclerait). - nav.js : needsModeRedirect (helper pur) renvoie a l'accueil si une page hors accueil n'a pas de mode valide memorise ; modeLabel ne ment plus (vide si inconnu) ; listener DOMContentLoaded garde derriere typeof document -> module importable en test pur. - page-payment.js : garde finale dans startCheckout (pas de soumission sans mode valide, retour accueil, panier conserve) + libelle recap honnete. - tests/js/nav.test.js : table de verite des helpers + regression (categories.html charge nav.js). Revue bmad-compliance : regression de flux normal identifiee (must_fix) corrigee. 92 tests JS verts.
This commit is contained in:
parent
545aa19cf1
commit
88c987c2b2
4 changed files with 112 additions and 11 deletions
|
|
@ -6,12 +6,38 @@
|
||||||
* element with [data-mode-badge] on the page.
|
* element with [data-mode-badge] on the page.
|
||||||
* - Sync the cart item count into any element with [data-cart-count].
|
* - Sync the cart item count into any element with [data-cart-count].
|
||||||
* - Handle the mode query-string on page load (welcome -> categories handoff).
|
* - 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 this module in every page that has a header.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMode, setMode, getCartCount } from './state.js';
|
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.
|
* Reads ?mode= from the current URL and persists it if present.
|
||||||
* Called once on DOMContentLoaded so that the welcome -> categories
|
* Called once on DOMContentLoaded so that the welcome -> categories
|
||||||
|
|
@ -20,7 +46,7 @@ import { getMode, setMode, getCartCount } from './state.js';
|
||||||
function syncModeFromURL() {
|
function syncModeFromURL() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const modeParam = params.get('mode');
|
const modeParam = params.get('mode');
|
||||||
if (modeParam === 'sur-place' || modeParam === 'a-emporter') {
|
if (VALID_MODES.includes(modeParam)) {
|
||||||
setMode(modeParam);
|
setMode(modeParam);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,8 +55,7 @@ function syncModeFromURL() {
|
||||||
* Renders the human-readable mode label into every [data-mode-badge] element.
|
* Renders the human-readable mode label into every [data-mode-badge] element.
|
||||||
*/
|
*/
|
||||||
function renderModeBadge() {
|
function renderModeBadge() {
|
||||||
const mode = getMode();
|
const label = modeLabel(getMode());
|
||||||
const label = mode === 'a-emporter' ? 'A emporter' : 'Sur place';
|
|
||||||
document.querySelectorAll('[data-mode-badge]').forEach(el => {
|
document.querySelectorAll('[data-mode-badge]').forEach(el => {
|
||||||
el.textContent = label;
|
el.textContent = label;
|
||||||
});
|
});
|
||||||
|
|
@ -48,9 +73,17 @@ export function refreshCartBadge() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialise on DOM ready */
|
/* 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
syncModeFromURL();
|
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();
|
renderModeBadge();
|
||||||
refreshCartBadge();
|
refreshCartBadge();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,17 @@ async function doSubmit(serviceTag) {
|
||||||
|
|
||||||
function startCheckout() {
|
function startCheckout() {
|
||||||
if (checkingOut) return;
|
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;
|
checkingOut = true;
|
||||||
if (getMode() === 'sur-place') {
|
if (mode === 'sur-place') {
|
||||||
openChevalet(tag => doSubmit(tag), () => { checkingOut = false; });
|
openChevalet(tag => doSubmit(tag), () => { checkingOut = false; });
|
||||||
} else {
|
} else {
|
||||||
doSubmit('');
|
doSubmit('');
|
||||||
|
|
@ -176,7 +185,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (recap) {
|
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 = `
|
recap.innerHTML = `
|
||||||
<p class="payment-recap__mode">${escHtml(modeLabel)}</p>
|
<p class="payment-recap__mode">${escHtml(modeLabel)}</p>
|
||||||
<p class="payment-recap__items">${items.length} article${items.length > 1 ? 's' : ''}</p>
|
<p class="payment-recap__items">${items.length} article${items.length > 1 ? 's' : ''}</p>
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,11 @@
|
||||||
</nav>
|
</nav>
|
||||||
</main>
|
</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>
|
<script type="module" src="assets/js/a11y.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
53
tests/js/nav.test.js
Normal file
53
tests/js/nav.test.js
Normal 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/);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue