diff --git a/tests/browser/accueil.mjs b/tests/browser/accueil.mjs index 9f51320..1e305dd 100644 --- a/tests/browser/accueil.mjs +++ b/tests/browser/accueil.mjs @@ -16,7 +16,7 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { chromium, firefox } from "playwright"; -import { hiddenUnderActionBar } from "./recette.mjs"; +import { hiddenUnderActionBar, tutorialWidthProblems, tutorialWidths } from "./recette.mjs"; const [base, browserName, shots, updateBase, updateDir] = process.argv.slice(2); const browserType = { chromium, firefox }[browserName]; @@ -171,6 +171,7 @@ async function checkTutorialView(page, name) { return b.scrollHeight - b.clientHeight; })(), height: innerHeight, + width: innerWidth, buttons, }; }); @@ -189,8 +190,13 @@ async function checkTutorialView(page, name) { } tutorialViews++; tutorialMinNumber = Math.min(tutorialMinNumber, view.minNumber); + // Largeurs du plateau et du texte, comparées d'une étape à l'autre à la + // fin du tutoriel (tutorialWidthProblems). + const widths = await tutorialWidths(page); + tutorialWidthViews.push({ name, size: `${view.width}x${view.height}`, ...widths }); } let tutorialViews = 0; +const tutorialWidthViews = []; let tutorialBodyScroll = { px: 0, where: "" }; let tutorialMinNumber = Infinity; @@ -331,6 +337,18 @@ async function playTouches(page, touches) { await until(page, "fin", () => location.hash === "#/tutoriel/fin"); await page.click("#tutorial-next"); await until(page, "première partie", () => location.hash === "#/" && window.__shown("game")); + if (full) { + // Les mesures, écran par écran, à côté des captures. + writeFileSync(`${dir}/tutoriel-largeurs.txt`, tutorialWidthViews.map((v) => + `${v.size}\t${v.name}\tplateau ${v.board.toFixed(1)}\ttexte ${v.text.toFixed(1)}\tpage ${v.page}`) + .join("\n") + "\n"); + const widthProblems = tutorialWidthProblems(tutorialWidthViews); + check(widthProblems.length === 0, `largeurs du tutoriel :\n ${widthProblems.join("\n ")}`); + const sizes = [...new Set(tutorialWidthViews.map((v) => v.size))]; + log(`tutoriel : plateau de même largeur à chaque étape (${sizes.map((s) => + `${s} : ${tutorialWidthViews.find((v) => v.size === s).board.toFixed(1)} px`).join(", ")}), ` + + "aucune page plus large que l'écran"); + } log(`tutoriel : ${screens} écrans, gestes refusés puis justes sur les 4 écrans « à vous », fin` + (full ? ` ; recette sur ${tutorialViews} vues, numéros de ${tutorialMinNumber.toFixed(1)} px au moins, ` + `texte défilant au plus de ${tutorialBodyScroll.px} px${tutorialBodyScroll.px > 0 ? ` (${tutorialBodyScroll.where})` : ""}` : "")); diff --git a/tests/browser/recette.mjs b/tests/browser/recette.mjs index a3d7b6f..ddc2658 100644 --- a/tests/browser/recette.mjs +++ b/tests/browser/recette.mjs @@ -98,3 +98,72 @@ export async function hiddenUnderActionBar(target) { return hidden; }); } + +/** + * Les largeurs d'un écran du tutoriel, en px CSS : le plateau, la colonne + * de texte, et la largeur de la page (scrollWidth) face à celle de l'écran. + * Une page plus large que l'écran défile de côté ; sur un téléphone, Chrome + * la dézoome pour la montrer en entier, et tout l'écran paraît rétréci. + */ +export async function tutorialWidths(target) { + return target.evaluate(() => { + const width = (selector) => { + const el = document.querySelector(selector); + return el === null ? 0 : el.getBoundingClientRect().width; + }; + const page = document.scrollingElement; + // Le plus à droite des descendants du tutoriel, pour nommer le + // coupable d'un débordement. + let widest = { what: "", right: 0 }; + for (const el of document.querySelectorAll("#tutorial *")) { + const r = el.getBoundingClientRect(); + if (r.width > 0 && r.right > widest.right) { + const name = el.id + ? `#${el.id}` + : `${el.tagName.toLowerCase()}${[...el.classList].map((c) => `.${c}`).join("")}`; + widest = { what: name, right: r.right }; + } + } + return { + board: width("#tutorial-board"), + text: width("#tutorial .tutorial-body"), + page: Math.max(page.scrollWidth, document.body.scrollWidth), + screen: innerWidth, + widest, + }; + }); +} + +/** + * Règle de la recette : pour une taille d'écran donnée, le plateau du + * tutoriel a la même largeur à toutes les étapes, et aucune page n'est plus + * large que l'écran. views : [{ name, size, board, text, page, screen, + * widest }]. Renvoie la liste des écarts, décrits (vide si tout va bien). + */ +export function tutorialWidthProblems(views) { + const problems = []; + const bySize = new Map(); + for (const v of views) { + bySize.set(v.size, [...(bySize.get(v.size) ?? []), v]); + if (v.page > v.screen + 0.5) { + problems.push( + `${v.name} en ${v.size} : page de ${v.page} px pour ${v.screen} px ` + + `(le plus à droite : ${v.widest.what}, ${Math.round(v.widest.right)} px)`, + ); + } + } + for (const [size, list] of bySize) { + const widths = list.map((v) => v.board); + const min = Math.min(...widths); + const max = Math.max(...widths); + if (max - min > 0.5) { + const odd = list + .filter((v) => Math.abs(v.board - max) > 0.5) + .map((v) => `${v.name} ${v.board.toFixed(1)}`); + problems.push( + `${size} : plateau de ${min.toFixed(1)} à ${max.toFixed(1)} px (${odd.join(", ")})`, + ); + } + } + return problems; +} diff --git a/web/app.css b/web/app.css index 368b25c..f1b8457 100644 --- a/web/app.css +++ b/web/app.css @@ -295,6 +295,15 @@ a { flex-direction: column; } +/* Les pages sont des éléments de la colonne flexible #screen. Leurs marges + * automatiques les centrent, mais annulent leur étirement en largeur : sans + * largeur explicite, une page au contenu court se réduit à la largeur de ce + * contenu (le plateau de l'étape 6 « à vous » du tutoriel passait de 396 à + * 300 px en 412 × 915). Leur max-width les borne toujours. */ +.screen > .page { + width: 100%; +} + .spacer { flex: 1; }