Files
Ustagammon/tests/browser/recette.mjs

101 lines
4.1 KiB
JavaScript

// 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;
});
}