Recette design du match : Crawford en paysage, « Nouvelle partie » en deux colonnes, « Rejouer »

- Paysage : pendant la partie Crawford, la consigne de la bande centrale
  commence par « Partie Crawford · » ; la police se resserre si besoin.
- Écran « Nouvelle partie » en paysage : deux colonnes au-dessus de la
  barre d'action (type de partie et longueur à gauche, niveau et conseil
  du maître à droite), tout tient en 915x412 et en 844x320.
- Fin de partie : « Rejouer » relance tout de suite une partie aux mêmes
  réglages (un match de même longueur après un match), « Partie suivante »
  dans un match qui continue, lien « Changer les réglages ».
- Recette automatique : aucun élément interactif visible sous une barre
  d'action collée, vérifié sur chaque écran capturé (tests/browser/recette.mjs).
- Scénario « Comment rejouer avec les mêmes réglages ? ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-10 05:03:59 +01:00
co-authored by Claude Opus 5.5
parent 67bd207337
commit 74693c4277
16 changed files with 484 additions and 51 deletions
+92 -12
View File
@@ -13,10 +13,13 @@
// joueur joue toujours le premier pion jouable vers sa première case
// d'arrivée, prend les doubles et ne double jamais. Captures de la recette
// design (Chromium) dans <dossier des captures>/match/ : écran « Nouvelle
// partie », score du match et partie Crawford, en 412×915 et 915×412.
// partie » (aussi en 844×320), score du match et partie Crawford, en
// 412×915 et 915×412. Sur chaque capture, rien d'interactif ne doit être
// caché sous la barre d'action collée (recette.mjs).
import { mkdirSync } from "node:fs";
import { chromium, firefox } from "playwright";
import { hiddenUnderActionBar } from "./recette.mjs";
const [base, browserName, shots] = process.argv.slice(2);
const browserType = { chromium, firefox }[browserName];
@@ -29,6 +32,7 @@ const dir = `${shots}/match`;
mkdirSync(dir, { recursive: true });
const PORTRAIT = { width: 412, height: 915 };
const LANDSCAPE = { width: 915, height: 412 };
const SMALL_LANDSCAPE = { width: 844, height: 320 };
// La graine des dés : un match qui passe par une partie Crawford.
const SEED = 7;
@@ -83,14 +87,18 @@ async function until(what, fn, arg, ms = 60_000) {
throw new Error(`attente trop longue : ${what}`);
}
/** Une capture de la recette, dans les deux tailles (Chromium seulement). */
async function capture(name) {
/** Une capture de la recette, dans chaque taille (Chromium seulement),
* après la règle commune : rien d'interactif sous la barre d'action. */
async function capture(name, sizes = [PORTRAIT, LANDSCAPE]) {
if (!full) {
return;
}
for (const size of [PORTRAIT, LANDSCAPE]) {
for (const size of sizes) {
await page.setViewportSize(size);
await sleep(200);
const hidden = await hiddenUnderActionBar(page);
check(hidden.length === 0,
`${name} ${size.width}x${size.height} : caché sous la barre d'action : ${hidden.join(", ")}`);
await page.screenshot({ path: `${dir}/${name}-${size.width}x${size.height}.png` });
captures++;
}
@@ -189,7 +197,45 @@ check((await page.getAttribute("#new-game-crawford a", "href")) === "#/glossaire
"lien vers la règle de Crawford");
check((await page.textContent("#new-game-level-text")).startsWith("Joue simplement"),
"description du niveau Débutant");
await capture("1-nouvelle-partie");
// Paysage : deux colonnes au-dessus de la barre d'action, tout tient sans
// défiler, jusqu'en 844×320 ; le type de partie à gauche, le niveau et le
// conseil du maître à droite.
for (const size of [LANDSCAPE, SMALL_LANDSCAPE]) {
await page.setViewportSize(size);
await sleep(200);
const where = `${size.width}x${size.height}`;
const layout = await page.evaluate(() => {
const box = (selector) => {
const r = document.querySelector(selector).getBoundingClientRect();
return { top: r.top, bottom: r.bottom, left: r.left, right: r.right };
};
return {
scroll: document.scrollingElement.scrollHeight,
height: innerHeight,
bar: box(".new-game-actions"),
kinds: box(".segmented"),
lengths: box(".lengths"),
rule: box("#new-game-crawford"),
bars: box(".level-bars"),
name: box("#new-game-level-name"),
text: box("#new-game-level-text"),
advice: box("#new-game-advice"),
};
});
check(layout.scroll <= layout.height + 1,
`${where} : l'écran « Nouvelle partie » défile (${layout.scroll} px pour ${layout.height})`);
for (const part of ["kinds", "lengths", "rule", "bars", "name", "text", "advice"]) {
check(layout[part].top >= 0 && layout[part].bottom <= layout.bar.top + 1,
`${where} : ${part} n'est pas au-dessus de la barre d'action : ${JSON.stringify(layout)}`);
}
for (const part of ["bars", "name", "text", "advice"]) {
check(layout[part].left >= layout.kinds.right,
`${where} : ${part} n'est pas dans la colonne de droite : ${JSON.stringify(layout)}`);
}
}
await page.setViewportSize(PORTRAIT);
await sleep(200);
await capture("1-nouvelle-partie", [PORTRAIT, LANDSCAPE, SMALL_LANDSCAPE]);
// Fermer ne change rien : on revient à la partie, et les choix sont gardés.
const before = await page.evaluate(() => localStorage.getItem("ustagammon.settings"));
await page.click("#new-game-close");
@@ -240,7 +286,13 @@ for (;;) {
const detail = await page.textContent("#panel-detail");
const primary = await page.textContent("#panel-primary");
log(`partie ${games}${crawford ? " (Crawford)" : ""} : ${title} ; ${detail}`);
if (primary === "Nouvelle partie") {
const settingsLink = await page.evaluate(() => {
const a = document.getElementById("panel-settings");
return a.hidden ? null : { text: a.textContent, href: a.getAttribute("href") };
});
check(settingsLink?.text === "Changer les réglages" && settingsLink.href === "#/nouvelle-partie",
`lien « Changer les réglages » : ${JSON.stringify(settingsLink)}`);
if (primary === "Rejouer") {
check(/gagne(z)? le match$/.test(title) && detail.includes("Score final"),
`fin du match : ${title} ; ${detail}`);
const [, player, computer] = /vous (\d+), l'ordinateur (\d+)/.exec(detail) ?? [];
@@ -254,20 +306,30 @@ for (;;) {
`score en paysage : « ${box} »`);
await page.setViewportSize(PORTRAIT);
await sleep(200);
// Après le match, « Nouvelle partie » mène à l'écran des choix.
await page.click("#panel-primary");
// Après le match, « Changer les réglages » mène à l'écran des
// choix ; le fermer ramène au résultat, où « Rejouer » relance
// tout de suite un match de même longueur.
await page.click("#panel-settings");
await until("écran « Nouvelle partie » après le match", () =>
location.hash === "#/nouvelle-partie");
location.hash === "#/nouvelle-partie" && document.getElementById("panel-veil").hidden);
await page.click("#new-game-close");
await until("retour au résultat du match", () => location.hash === "#/" &&
!document.getElementById("again").hidden);
check((await page.textContent("#again")) === "Rejouer",
`après le match, barre d'actions : « ${await page.textContent("#again")} »`);
log(`match ${matches} fini : ${games} parties, ${crawfordGames} Crawford au total`);
await page.click("#again");
await until("match rejoué", () => location.hash === "#/" &&
!document.body.classList.contains("busy"));
const again = await shownMatch();
check(again.line === "Match en 3 points" && again.computer === "0" && again.player === "0",
`« Rejouer » après le match : ${JSON.stringify(again)}`);
if (crawfordGames > 0 && scoreShot) {
break;
}
check(matches < 5, `aucune partie Crawford en ${matches} matchs (graine ${SEED})`);
matches++;
games = 1;
await page.click("#new-game-start");
await until("match suivant", () => location.hash === "#/" &&
!document.body.classList.contains("busy"));
continue;
}
check(primary === "Partie suivante" && /Match en 3 points : vous \d+, l'ordinateur \d+/.test(detail),
@@ -281,6 +343,24 @@ for (;;) {
check(next.cube === 0, "partie Crawford : le videau est dessiné");
if (!crawfordShot) {
crawfordShot = true;
check(next.line === "Match en 3 points · partie Crawford",
`partie Crawford, portrait : « ${next.line} »`);
await page.setViewportSize(LANDSCAPE);
await sleep(200);
const band = await page.evaluate(() => {
const text = document.getElementById("band-text");
const pill = document.querySelector("#board .band-pill");
return {
text: text?.textContent ?? "",
fits: text !== null && pill !== null &&
text.getBBox().width <= pill.getBBox().width - 16,
};
});
check(band.text.startsWith("Partie Crawford · "),
`partie Crawford, paysage : « ${band.text} »`);
check(band.fits, `partie Crawford, paysage : la consigne déborde (« ${band.text} »)`);
await page.setViewportSize(PORTRAIT);
await sleep(200);
await capture("3-partie-crawford");
}
} else if (!scoreShot) {