// recette.mjs -- règles de la recette design communes aux tests dans un // navigateur (play.mjs, accueil.mjs, match.mjs), vérifiées sur chaque écran // capturé. // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). /** * Les boutons visibles (et les liens qui en ont l'allure) plus hauts que * 56 px : un bouton fait 48 px, il ne s'étire pas. Renvoie la liste des * boutons trop hauts, décrits (vide si tout va bien). */ export async function tallButtons(target) { return target.evaluate(() => { const found = []; for (const el of document.querySelectorAll("button, a.button")) { if (el.closest("[hidden]")) { continue; } const style = getComputedStyle(el); const r = el.getBoundingClientRect(); if (style.display === "none" || style.visibility === "hidden" || r.width === 0) { continue; } if (r.height > 56) { const name = el.id ? `#${el.id}` : `« ${el.textContent.trim().slice(0, 30)} »`; found.push(`${name} (${Math.round(r.height)} px)`); } } return found; }); } /** * Les éléments interactifs visibles cachés, même en partie, sous une barre * d'action collée en bas de l'écran (position: sticky, ancrée par bottom) : * un bouton, un lien ou un interrupteur que la barre recouvre ne se voit * pas et ne se touche pas. Un élément entièrement sous le bas de l'écran * n'est pas visible : il n'est pas compté. Ce qui passe devant la barre (le * menu, un panneau) n'est pas caché : seul compte ce que la barre recouvre * vraiment, au point du recouvrement. * * Renvoie la liste des éléments cachés, décrits (vide si tout va bien). */ export async function hiddenUnderActionBar(target) { return target.evaluate(() => { const shown = (el) => { if (el.closest("[hidden]")) { return false; } const style = getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden") { return false; } const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; }; const bars = [...document.querySelectorAll("body *")].filter((el) => { const style = getComputedStyle(el); return style.position === "sticky" && style.bottom !== "auto" && shown(el); }); const describe = (el) => el.id ? `#${el.id}` : `${el.tagName.toLowerCase()}${[...el.classList].map((c) => `.${c}`).join("")} « ${ el.getAttribute("aria-label") ?? el.textContent.trim().slice(0, 40) } »`; const hidden = []; const interactive = document.querySelectorAll( "button, a[href], input, select, textarea, [role=switch], [role=radio], " + "[tabindex]:not([tabindex='-1'])", ); for (const bar of bars) { const b = bar.getBoundingClientRect(); for (const el of interactive) { if (bar.contains(el) || el.contains(bar) || !shown(el)) { continue; } const r = el.getBoundingClientRect(); if (r.top >= innerHeight) { continue; } const top = Math.max(r.top, b.top); const bottom = Math.min(r.bottom, b.bottom, innerHeight); const left = Math.max(r.left, b.left); const right = Math.min(r.right, b.right); if (bottom - top <= 1 || right - left <= 1) { continue; } const hit = document.elementFromPoint((left + right) / 2, (top + bottom) / 2); if (hit !== null && bar.contains(hit)) { hidden.push( `${describe(el)} sous ${describe(bar)} (${Math.round(bottom - top)} px)`, ); } } } return hidden; }); }