Files
Ustagammon/tests/browser/accueil.mjs
T
Pierre MARTINandClaude Opus 5.5 351482fa9e Recette 1g : textes corrigés, boutons en bas, barre haute en paysage
Leçon 1 §2 (flèche 7 vide, 5 déjà bloqué), leçon 2 « Faire des points »,
flèches 7 à 12, reformulations des leçons 3 et 4. Écran des leçons et du
tutoriel : « Retour » et « Continuer » en bas en portrait, « Revoir » sur
la ligne du titre, ligne « Leçon n · … » gardée en paysage. Barre haute
visible hors de l'écran de jeu en paysage (défaut antérieur à 1g), avec
un contrôle dans la recette automatique. Rappel des règles haut de
44 px ; « Aucune leçon terminée sur 5 · … » sur l'accueil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 13:55:17 +01:00

571 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// accueil.mjs -- l'accueil, le tutoriel des règles, l'aide, l'installation
// et la mise à jour de l'application, dans un vrai navigateur sans écran
// (Playwright), sur le site servi par l'image de production
// (docs/plans/2026-11-03-accueil-tutoriel-aide.md, point 7).
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
//
// Usage : node accueil.mjs <adresse du site> <chromium|firefox> <dossier des
// captures> <adresse du site modifiable> <dossier de ce site>
// Lancé par tests/browser.sh. Le second site sert la même chose depuis un
// dossier que ce script modifie : il y publie une deuxième version pour
// essayer la mise à jour. Captures de la recette design (Chromium) dans
// <dossier des captures>/accueil-tutoriel/, en 412×915 et 915×412, et aussi
// en 844×320 pour le tutoriel.
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { chromium, firefox } from "playwright";
import { hiddenUnderActionBar, missingTopbar, tutorialWidthProblems, tutorialWidths } from "./recette.mjs";
const [base, browserName, shots, updateBase, updateDir] = process.argv.slice(2);
const browserType = { chromium, firefox }[browserName];
if (!base || !browserType || !shots || !updateBase || !updateDir) {
console.error("usage : node accueil.mjs <adresse> <chromium|firefox> <captures> <adresse 2> <dossier 2>");
process.exit(2);
}
const full = browserName === "chromium";
const dir = `${shots}/accueil-tutoriel`;
mkdirSync(dir, { recursive: true });
const SIZES = [
[412, 915],
[915, 412],
];
// Le tutoriel est aussi vérifié dans le plus petit paysage de la recette.
const TUTORIAL_SIZES = [...SIZES, [844, 320]];
const browser = await browserType.launch();
const problems = [];
const log = (text) => console.log(`[${browserName}] ${text}`);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let captures = 0;
function check(condition, message) {
if (!condition) {
throw new Error(message);
}
}
/** Un contexte neuf (stockage vide), avec des réglages déjà enregistrés si
* settings est donné. Erreurs de la page et violations de la CSP relevées. */
async function newPage(settings = null, url = base) {
const context = await browser.newContext({
viewport: { width: 412, height: 915 },
deviceScaleFactor: 1,
locale: "fr-FR",
});
await context.addInitScript((saved) => {
window.__csp = [];
document.addEventListener("securitypolicyviolation", (e) =>
window.__csp.push(`${e.effectiveDirective} ${e.blockedURI}`),
);
// Un élément est-il à l'écran ? (Utilisé par les attentes.)
window.__shown = (id) => {
const el = document.getElementById(id);
return el !== null && !el.closest("[hidden]") && el.getClientRects().length > 0;
};
window.__vibrations = [];
Object.defineProperty(Navigator.prototype, "vibrate", {
configurable: true,
value: (pattern) => {
window.__vibrations.push(String(pattern));
return true;
},
});
if (saved !== null && localStorage.getItem("ustagammon.settings") === null) {
localStorage.setItem("ustagammon.settings", saved);
}
}, settings === null ? null : JSON.stringify(settings));
const page = await context.newPage();
page.on("pageerror", (error) => problems.push(`erreur de la page : ${error.message}`));
page.on("console", (message) => {
if (message.type() === "error") {
problems.push(`console : ${message.text()}`);
}
});
page.on("dialog", (dialog) => {
problems.push(`boîte native : ${dialog.type()}`);
void dialog.dismiss();
});
await page.goto(url);
return { context, page };
}
/** Attend que test (évalué dans la page) soit vrai, au plus ms. (Pas de
* waitForFunction : Playwright y passe par eval, que la CSP interdit.) */
async function until(page, what, test, arg, ms = 30_000) {
let last = null;
for (const end = Date.now() + ms; Date.now() < end; ) {
last = await page.evaluate(test, arg).catch((error) => `page illisible : ${error.message}`);
if (last === true) {
return;
}
await sleep(100);
}
await page.screenshot({ path: `${shots}/${browserName}-accueil-echec.png` }).catch(() => {});
throw new Error(`${what} : attente dépassée (${JSON.stringify(last)})`);
}
/** Range une capture de l'écran courant dans chaque taille (Chromium). */
async function capture(page, name, settle = async () => {}, sizes = SIZES) {
if (!full) {
return;
}
for (const [width, height] of sizes) {
await page.setViewportSize({ width, height });
await sleep(250);
await settle();
const hidden = await hiddenUnderActionBar(page);
check(hidden.length === 0, `${name} ${width}x${height} : caché sous la barre d'action : ${hidden.join(", ")}`);
const topbar = await missingTopbar(page);
check(topbar === null, `${name} : ${topbar}`);
await page.screenshot({ path: `${dir}/${name}-${width}x${height}.png`, fullPage: true });
captures++;
}
await page.setViewportSize({ width: 412, height: 915 });
await sleep(150);
}
/** Vérifie la CSP et ferme le contexte. */
async function close(context, page) {
const csp = await page.evaluate(() => window.__csp).catch(() => []);
check(csp.length === 0, `violations de la CSP : ${csp.join(", ")}`);
await context.close();
}
// ------------------------------------------------- Premier lancement
/** L'écran du tutoriel prêt : plateau dessiné pour cette adresse, coup
* illustré joué (« Revoir » utilisable) s'il y en a un. */
async function tutorialReady(page, hash) {
await until(page, `tutoriel ${hash}`, (hash) => {
const section = document.getElementById("tutorial");
const replay = document.getElementById("tutorial-replay");
return location.hash === hash && section.dataset.ready !== undefined &&
document.querySelectorAll("#tutorial-board polygon[data-point]").length === 24 &&
(replay.hidden || !replay.disabled);
}, hash);
}
/** Recette du tutoriel dans la taille courante : numéros des flèches de
* 12 px au moins ; en paysage, rien ne défile et les boutons sont en vue. */
async function checkTutorialView(page, name) {
const view = await page.evaluate(() => {
const svg = document.getElementById("tutorial-board");
const box = svg.getBoundingClientRect();
const vb = svg.viewBox.baseVal;
const scale = Math.min(box.width / vb.width, box.height / vb.height);
const numbers = [...svg.querySelectorAll(".point-number")]
.map((t) => Number(t.getAttribute("font-size")) * scale);
const buttons = ["tutorial-back", "tutorial-next", "tutorial-drop", "tutorial-take"]
.map((id) => document.getElementById(id))
.filter((el) => el !== null && el.closest("[hidden]") === null)
.map((el) => {
const r = el.getBoundingClientRect();
return { id: el.id, top: r.top, bottom: r.bottom };
});
return {
landscape: document.documentElement.classList.contains("landscape"),
minNumber: numbers.length === 0 ? 0 : Math.min(...numbers),
scroll: document.scrollingElement.scrollHeight - innerHeight,
body: (() => {
const b = document.querySelector("#tutorial .tutorial-body");
return b.scrollHeight - b.clientHeight;
})(),
height: innerHeight,
width: innerWidth,
buttons,
};
});
const size = `${name} en ${await page.evaluate(() => `${innerWidth}x${innerHeight}`)}`;
check(view.minNumber >= 12, `${size} : numéros des flèches de ${view.minNumber.toFixed(1)} px`);
if (view.landscape) {
check(view.scroll <= 1, `${size} : la page défile de ${view.scroll} px`);
check(view.body <= 1, `${size} : le texte défile de ${view.body} px`);
if (view.body > tutorialBodyScroll.px) {
tutorialBodyScroll = { px: view.body, where: size };
}
for (const b of view.buttons) {
check(b.top >= 0 && b.bottom <= view.height + 0.5,
`${size} : « ${b.id} » hors de l'écran (${b.top}-${b.bottom} pour ${view.height})`);
}
}
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;
/** Capture d'un écran du tutoriel, recette comprise, dans les trois tailles. */
async function tutorialCapture(page, name) {
await capture(page, name, () => checkTutorialView(page, name), TUTORIAL_SIZES);
}
/** Touche une case du plateau du tutoriel. */
async function touch(page, place) {
await page.click(`#tutorial-board [data-place="${place}"]`);
await sleep(200);
}
async function verdict(page) {
await until(page, "verdict", () => {
const v = document.getElementById("tutorial-feedback").dataset.verdict;
return v === "right" || v === "wrong";
});
return page.evaluate(() => document.getElementById("tutorial-feedback").dataset.verdict);
}
/** Les gestes des écrans « à vous » : un coup refusé, puis le bon. */
const PRACTICE = {
// 24/21 : ne peut plus mener à 8/5 6/5, refusé tout de suite.
3: { wrong: [["p24", "p21"]], right: [["p8", "p5"], ["p6"]] },
// 24/23 puis 24/18 : un coup complet qui ne frappe pas.
4: { wrong: [["p24", "p23"], ["p24"]], right: [["p13"], ["p8"]] },
// 6/3 : ne sort pas deux pions.
6: { wrong: [["p6", "p3"]], right: [["p6", "off-player"], ["p3"]] },
};
async function playTouches(page, touches) {
for (const group of touches) {
for (const place of group) {
await touch(page, place);
}
await until(page, "demi-coup joué", () => !document.querySelector("#tutorial[data-busy]"));
await sleep(150);
}
}
{
const { context, page } = await newPage();
await until(page, "premier lancement", () =>
location.hash === "#/accueil" && !document.getElementById("home-welcome").hidden);
const first = await page.evaluate(() => ({
topbar: getComputedStyle(document.querySelector(".topbar")).display,
links: [...document.querySelectorAll("#home-welcome a.choice")].map((a) => a.getAttribute("href")),
main: document.getElementById("home-main").hidden,
}));
check(first.topbar === "none" && first.main, `premier lancement : ${JSON.stringify(first)}`);
check(JSON.stringify(first.links) === '["#/tutoriel/1","#/"]', `choix : ${first.links}`);
await capture(page, "01-premier-lancement");
log("premier lancement : « Je découvre » et « Je connais les règles », de vrais liens");
// Le tutoriel complet, par ses liens « Continuer ».
await page.click("#welcome-discover");
let screens = 0;
for (let step = 1; step <= 8; step++) {
const hash = `#/tutoriel/${step}`;
await tutorialReady(page, hash);
// La barre ne mène qu'aux étapes déjà vues.
const links = await page.evaluate(() =>
[...document.querySelectorAll("#tutorial-progress a")].map((a) => a.getAttribute("href")));
const expected = Array.from({ length: step - 1 }, (_, i) => `#/tutoriel/${i + 1}`);
check(JSON.stringify(links) === JSON.stringify(expected),
`étape ${step} : barre de progression ${JSON.stringify(links)}`);
const illustrated = await page.evaluate(() => !document.getElementById("tutorial-replay").hidden);
if (illustrated) {
check(await page.locator("#tutorial-board .trace-arrow").count() > 0,
`étape ${step} : le coup illustré n'a pas laissé de traces`);
// « Revoir » rejoue le coup.
await page.click("#tutorial-replay");
await tutorialReady(page, hash);
}
if (step === 2) {
check(await page.locator("#tutorial-board .tutorial-path .direction-line").count() === 2,
"étape 2 : pas les deux flèches du sens du jeu");
// Sous les pions (détail : retours-joueurs.mjs).
const order = await page.evaluate(() => {
const children = [...document.getElementById("tutorial-board").children];
const at = (selector) => children.findIndex((c) => c.matches(selector));
return { path: at(".tutorial-path"), pieces: at("g.pieces") };
});
check(order.path >= 0 && order.path < order.pieces,
`étape 2 : les flèches passent au-dessus des pions ${JSON.stringify(order)}`);
}
await tutorialCapture(page, `tutoriel-${step}`);
screens++;
await page.click("#tutorial-next");
const practice = await page.evaluate(() => location.hash.endsWith("/a-vous"));
if (!practice) {
continue;
}
const avous = `${hash}/a-vous`;
await tutorialReady(page, avous);
check(await page.evaluate(() => document.getElementById("tutorial-next").classList.contains("secondary")),
`étape ${step}, à vous : « Continuer » principal avant le geste`);
await tutorialCapture(page, `tutoriel-${step}-a-vous`);
if (step === 8) {
await page.click("#tutorial-drop");
check(await verdict(page) === "wrong", "étape 8 : « Passer » accepté");
await tutorialCapture(page, "tutoriel-8-a-vous-refuse");
await page.click("#tutorial-take");
await until(page, "étape 8 juste", () =>
document.getElementById("tutorial-feedback").dataset.verdict === "right");
} else {
const moves = PRACTICE[step];
await playTouches(page, moves.wrong);
check(await verdict(page) === "wrong", `étape ${step} : coup faux accepté`);
// Le pion revient : de nouveau des pions jouables.
await until(page, "pion revenu", () => document.querySelectorAll("#tutorial-board .hit.can-move").length > 0);
const words = await page.textContent("#tutorial-feedback");
check(!/erreur/i.test(words), `étape ${step} : « erreur » dans le refus`);
await tutorialCapture(page, `tutoriel-${step}-a-vous-refuse`);
await playTouches(page, moves.right);
check(await verdict(page) === "right", `étape ${step} : bon coup refusé`);
check(await page.locator("#tutorial-board .hit.can-move").count() === 0,
`étape ${step} : plateau encore jouable après le bon coup`);
}
check(!(await page.evaluate(() => document.getElementById("tutorial-next").classList.contains("secondary"))),
`étape ${step} : « Continuer » pas principal après le bon geste`);
const vibrated = await page.evaluate(() => window.__vibrations.at(-1));
check(vibrated === "12", `étape ${step} : vibration ${vibrated}`);
await tutorialCapture(page, `tutoriel-${step}-a-vous-reussi`);
screens++;
await page.click("#tutorial-next");
}
await until(page, "fin du tutoriel", () => location.hash === "#/tutoriel/fin" &&
document.getElementById("tutorial").dataset.ready !== undefined);
await tutorialCapture(page, "tutoriel-fin");
// Le retour du téléphone : l'écran d'avant.
await page.goBack();
await tutorialReady(page, "#/tutoriel/8/a-vous");
await page.goForward();
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})` : ""}` : ""));
// Le choix ne revient plus : l'accueil, ensuite.
await page.goto(base);
await until(page, "accueil", () => location.hash === "#/accueil" &&
document.getElementById("home-welcome").hidden && !document.getElementById("home-main").hidden);
await close(context, page);
}
// --------------------------------------------------- Accueil, installation
{
// Une partie terminée : l'invitation à installer apparaît.
const settings = { welcomed: true, gamesStarted: 2, gamesFinished: 1, tutorialSeen: 99 };
const { context, page } = await newPage(settings);
await until(page, "accueil prêt", () => location.hash === "#/accueil" &&
!document.getElementById("home-new").disabled && !document.getElementById("install-card").hidden);
const home = await page.evaluate(() => ({
resume: document.getElementById("home-resume").getAttribute("href"),
primaries: [...document.querySelectorAll("#home a.button:not(.secondary), #home button:not(.secondary):not(.icon-button)")]
.filter((el) => !el.closest("[hidden]")).length,
rules: document.getElementById("home-rules").getAttribute("href"),
}));
check(home.resume === "#/" && home.primaries === 1 && home.rules === "#/tutoriel/1",
`accueil : ${JSON.stringify(home)}`);
await capture(page, "02-accueil");
await capture(page, "03-carte-installation", async () => {
await page.locator("#install-card").scrollIntoViewIfNeeded();
});
// Sans invite du navigateur : la marche à suivre, une feuille avec son
// adresse, que le retour referme.
await page.click("#install-card-button");
await until(page, "feuille d'installation", () => location.hash === "#/installer" && window.__shown("install-sheet"));
check((await page.locator("#install-steps li").count()) === 3, "marche à suivre sans trois étapes");
await capture(page, "04-feuille-installation");
await page.goBack();
await until(page, "feuille refermée", () => location.hash === "#/accueil" && !window.__shown("install-sheet"));
if (browserName === "chromium") {
// L'invite de Chrome, simulée : « Installer » l'ouvre.
await page.evaluate(() => {
window.__prompted = 0;
const event = new Event("beforeinstallprompt", { cancelable: true });
event.prompt = () => {
window.__prompted++;
return Promise.resolve();
};
event.userChoice = Promise.resolve({ outcome: "dismissed" });
window.dispatchEvent(event);
});
await page.click("#install-card-button");
await until(page, "invite du navigateur", () => window.__prompted === 1);
check(await page.evaluate(() => location.hash) === "#/accueil", "l'invite a ouvert la feuille");
// Installée : plus aucune proposition.
await page.evaluate(() => window.dispatchEvent(new Event("appinstalled")));
await until(page, "installée", () => document.getElementById("install-card").hidden &&
document.getElementById("menu-install").hidden);
log("installation : invite de Chrome simulée, puis plus rien une fois installée");
} else {
await page.click("#install-later");
await until(page, "plus tard", () => document.getElementById("install-card").hidden);
log("installation : marche à suivre, « Plus tard »");
}
await close(context, page);
}
// ------------------------------------------------------------------- Aide
{
const { context, page } = await newPage({ welcomed: true, gamesStarted: 2 }, `${base}#/aide`);
await until(page, "aide", () => window.__shown("help-play") && document.getElementById("help-content").dataset.loaded === "true");
const help = await page.evaluate(() => ({
steps: document.querySelectorAll("#help-play .steps, #help-play li").length,
questions: [...document.querySelectorAll("#help-play .question h3")].map((h) => h.textContent),
tutorial: document.getElementById("help-tutorial")?.getAttribute("href"),
}));
check(help.steps === 0, "aide : les étapes Gherkin sont affichées");
check(help.questions.length >= 25 && help.tutorial === "#/tutoriel/1", `aide : ${JSON.stringify(help)}`);
check(help.questions.filter((q) => q.endsWith("?")).length >= 20, "aide : trop peu de questions");
await capture(page, "05-aide-jouer");
const question = await page.evaluate(() =>
[...document.querySelectorAll("#help-play .question")].find((q) => q.querySelector(".see-board"))?.id);
await page.click(`#${question} h3 a`);
await until(page, "question ouverte", (id) => document.getElementById(id).classList.contains("open"), question);
await capture(page, "06-aide-question");
// Le retour du téléphone referme la question.
await page.goBack();
await until(page, "question refermée", () => location.hash === "#/aide" && !document.querySelector(".question.open"));
await page.click('#help-tabs a[data-screen="glossary"]');
await until(page, "glossaire", () => window.__shown("help-glossary"));
const before = await page.locator("#help-glossary .term-card:not([hidden])").count();
await page.fill("#glossary-search", "sortir");
await sleep(200);
const after = await page.locator("#help-glossary .term-card:not([hidden])").count();
check(after > 0 && after < before, `recherche : ${after} fiches sur ${before}`);
await capture(page, "07-aide-glossaire-recherche");
await page.click('#help-tabs a[data-screen="about"]');
await until(page, "à propos", () => window.__shown("help-about"));
const version = await page.textContent("#about-version");
check(/^Version publiée : [0-9a-f]{12}\.$/.test(version), `version : « ${version} »`);
await capture(page, "08-aide-a-propos");
log(`aide : ${help.questions.length} questions, recherche dans le glossaire (${after}/${before}), ${version}`);
await close(context, page);
}
// ----------------------------------------------------------- Mise à jour
if (browserName === "chromium") {
const { context, page } = await newPage({ welcomed: true, gamesStarted: 2 }, `${updateBase}#/`);
await until(page, "partie prête", () => !document.body.classList.contains("busy"), undefined, 60_000);
// Le service worker installé et aux commandes (première installation :
// sans attendre l'accord du joueur).
await until(page, "service worker", async () => {
const reg = await navigator.serviceWorker.getRegistration();
return reg?.active?.state === "activated" && navigator.serviceWorker.controller !== null;
}, undefined, 120_000);
const journal = () => page.evaluate(() => localStorage.length >= 0 &&
new Promise((resolve) => {
const open = indexedDB.open("ustagammon");
open.onsuccess = () => {
const db = open.result;
const tx = db.transaction(["settings"]);
const current = tx.objectStore("settings").get("current");
current.onsuccess = () => {
db.close();
resolve(current.result?.value ?? null);
};
};
open.onerror = () => resolve(null);
}));
check(await page.isHidden("#update-banner"), "bandeau de mise à jour sans nouvelle version");
// Si vous avez la main (jet d'ouverture), le bandeau attend la fin du
// tour : on publie la nouvelle version d'abord, puis on joue le coup.
const toPlay = () => page.evaluate(() => {
if (document.body.classList.contains("busy")) {
return "busy";
}
const usable = (id) => {
const b = document.getElementById(id);
return b !== null && !b.hidden && !b.disabled;
};
for (const id of ["roll", "again", "confirm", "skip", "take"]) {
if (usable(id)) {
return id;
}
}
if (document.querySelector("#board .hit.can-land")) {
return "land";
}
return document.querySelector("#board .hit.can-move") ? "move" : "busy";
});
// Une deuxième version publiée : autre version.json, autre sw.js.
writeFileSync(`${updateDir}/version.json`, '{"commit": "0123456789abcdef0123", "dirty": false}\n');
const sw = readFileSync(`${updateDir}/sw.js`, "utf8");
writeFileSync(`${updateDir}/sw.js`, sw.replace(/^const VERSION = "[0-9a-f]+";$/m, 'const VERSION = "0000000000000002";'));
await page.click("#menu-button");
await page.waitForSelector("#menu", { state: "visible" });
await page.click("#menu-update");
await until(page, "nouvelle version trouvée", () =>
document.getElementById("update-text")?.textContent !== "Recherche d'une mise à jour…", undefined, 120_000);
// Pendant votre coup, jamais de bandeau : seulement entre deux tours.
let waited = false;
for (let i = 0; i < 40; i++) {
const todo = await toPlay();
if (todo === "roll" || todo === "again") {
break;
}
if (todo === "busy") {
await sleep(100);
continue;
}
if (todo === "move" || todo === "land" || todo === "confirm") {
await sleep(400);
check(await page.isHidden("#update-banner"), `bandeau de mise à jour pendant votre coup (${todo})`);
waited = true;
}
await page.click(todo === "move" ? "#board .hit.can-move" : todo === "land" ? "#board .hit.can-land" : `#${todo}`);
await sleep(200);
}
await until(page, "bandeau de mise à jour", () => window.__shown("update-banner") && window.__shown("update-apply"), undefined, 120_000);
check((await page.textContent("#update-text")) === "Nouvelle version prête.",
`bandeau : « ${await page.textContent("#update-text")} »`);
// En bas, au-dessus de la barre d'actions, comme « Reprendre l'ancienne ».
const place = await page.evaluate(() => {
const banner = document.getElementById("update-banner").getBoundingClientRect();
const actions = document.getElementById("actions").getBoundingClientRect();
const top = document.querySelector(".topbar").getBoundingClientRect();
return { bottom: banner.bottom, top: banner.top, actions: actions.top, bar: top.bottom };
});
check(place.bottom <= place.actions && place.top > place.bar,
`bandeau de mise à jour mal placé : ${JSON.stringify(place)}`);
const gameBefore = await journal();
await capture(page, "09-mise-a-jour");
await Promise.all([page.waitForEvent("load", { timeout: 60_000 }), page.click("#update-apply")]);
await until(page, "partie reprise", () => location.hash === "#/" && !document.body.classList.contains("busy"), undefined, 60_000);
check(await journal() === gameBefore, "après la mise à jour, ce n'est plus la même partie");
await page.goto(`${updateBase}#/a-propos`);
await until(page, "nouvelle version", () =>
document.getElementById("about-version")?.textContent === "Version publiée : 0123456789ab.");
log(`mise à jour : deuxième version publiée, bandeau ${waited ? "retenu pendant votre coup, puis " : ""}entre deux tours, « Mettre à jour », partie retrouvée, nouvelle version`);
// Rien de neuf : « Vous avez la dernière version ».
await page.goto(`${updateBase}#/`);
await until(page, "partie", () => !document.body.classList.contains("busy"), undefined, 60_000);
await page.click("#menu-button");
await page.waitForSelector("#menu", { state: "visible" });
await page.click("#menu-update");
await until(page, "dernière version", () =>
document.getElementById("update-text").textContent === "Vous avez la dernière version.", undefined, 60_000);
await close(context, page);
}
await browser.close();
if (problems.length > 0) {
console.error(problems.join("\n"));
process.exit(1);
}
log(`aucune erreur dans la page${full ? ` ; ${captures} captures dans accueil-tutoriel/` : ""}`);