Recette 1h : courbe, lignes de partie, en-tête, bouton des exercices, carte de l'accueil
Dernier point plus gros, vert seulement si le taux baisse ; valeurs au premier et au dernier point, axe réduit à la ligne du 0. Une ligne de partie = un lien vers le bilan, résultat court, date et niveau en caption. Plus de « Retour », titre en h1 ; « Nouvelle partie » pleine largeur en portrait ; « Demander des exercices à une IA ». La carte de l'accueil sans partie en cours ne montre plus la partie finie (testé). Phrase progression.hausse sans « un peu ». L'écran relit ses parties à chaque visite sans montrer celles de la précédente. Test navigateur sur trois courses jouées jusqu'au bout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2664486541
commit
c2c5d13651
+101
-53
@@ -7,14 +7,17 @@
|
||||
//
|
||||
// Usage : node progression.mjs <adresse du site> <chromium|firefox> <dossier des captures>
|
||||
// Lancé par tests/browser.sh. L'écran sans partie terminée, puis trois
|
||||
// fins de course courtes jouées par l'interface, depuis un lien de
|
||||
// position, avec des dés imposés (comme bilan.mjs) : le joueur a 6-1 et
|
||||
// deux pions, sur 6 et 1 ; il joue 6/5/off (grosse erreur, l'ordinateur
|
||||
// double et le joueur passe), puis 6/off 1/off (gagne), puis de nouveau
|
||||
// 6/5/off. Vérifie que la ligne de l'accueil mène à #/progression, que la
|
||||
// courbe a trois points égaux, dans l'ordre, aux taux d'erreur des trois
|
||||
// bilans, la phrase du maître, la liste reliée aux bilans et aux analyses,
|
||||
// et « Créer des exercices » (feuille du prompt). Rien d'interactif sous
|
||||
// courses courtes jouées jusqu'au bout par l'interface, depuis un lien de
|
||||
// position (celle des tests du domaine), dés d'une graine fixe : le joueur
|
||||
// joue toujours le premier pion jouable et la première case d'arrivée, et
|
||||
// prend les doubles, ce qui donne plusieurs décisions par partie et des
|
||||
// taux d'erreur réalistes. Vérifie que la ligne de l'accueil (44 px au
|
||||
// moins) mène à #/progression, que la carte de la partie ne montre plus la
|
||||
// partie finie, que la courbe a trois points égaux, dans l'ordre, aux taux
|
||||
// d'erreur des trois bilans, le dernier point en vert seulement si le taux
|
||||
// baisse, la phrase du maître, la liste (un lien par partie, vers son
|
||||
// bilan) et « Demander des exercices à une IA » (feuille du prompt). Rien
|
||||
// d'interactif sous
|
||||
// une barre d'action, aucune erreur ni violation de la politique de
|
||||
// sécurité. Captures (Chromium) dans <dossier des captures>/progression/,
|
||||
// en 412×915 et 915×412.
|
||||
@@ -35,9 +38,8 @@ mkdirSync(dir, { recursive: true });
|
||||
const PORTRAIT = { width: 412, height: 915 };
|
||||
const LANDSCAPE = { width: 915, height: 412 };
|
||||
const SIZES = [PORTRAIT, LANDSCAPE];
|
||||
// Le joueur au trait : un pion sur 6, un sur 1 ; l'ordinateur : un sur 1,
|
||||
// un sur 2 (positions de gnubg, chacun vu de lui-même).
|
||||
const BEAROFF = "BQAACAIAAAAAAA";
|
||||
// Une course courte (Position ID de gnubg, app/tests/progression.test.ts).
|
||||
const RACE = "trsXAADs7mwAAA";
|
||||
|
||||
const problems = [];
|
||||
const log = (text) => console.log(`[${browserName}] ${text}`);
|
||||
@@ -95,7 +97,6 @@ async function until(what, fn, arg, ms = 120_000) {
|
||||
throw new Error(`attente trop longue : ${what}`);
|
||||
}
|
||||
|
||||
const idle = () => until("écran libre", () => !document.body.classList.contains("busy"));
|
||||
|
||||
/** Une capture de la recette dans les deux tailles (Chromium seulement),
|
||||
* après la règle commune ; settle replace l'écran après le changement de
|
||||
@@ -141,26 +142,49 @@ async function startFrom(position) {
|
||||
);
|
||||
}
|
||||
|
||||
const place = (point) => (point === 0 ? "off-player" : `p${point}`);
|
||||
|
||||
/** Lance les dés imposés et joue un coup, demi-coup par demi-coup. */
|
||||
async function play(dice, halfMoves) {
|
||||
await page.evaluate((d) => {
|
||||
window.__dice.push(...d);
|
||||
}, dice);
|
||||
await page.click("#roll");
|
||||
await idle();
|
||||
for (const [from, to] of halfMoves) {
|
||||
await page.click(`#board .hit[data-place="${place(from)}"]`);
|
||||
await idle();
|
||||
const land = page.locator(`#board .hit.can-land[data-place="${place(to)}"]`);
|
||||
if ((await land.count()) > 0) {
|
||||
await land.click();
|
||||
await idle();
|
||||
/** Joue la partie jusqu'au bout : lancer, le premier pion jouable et sa
|
||||
* première case d'arrivée, valider ; prendre les doubles ; passer quand
|
||||
* aucun coup n'est possible. */
|
||||
async function playOut() {
|
||||
for (let step = 0; step < 2000; step++) {
|
||||
const s = await page.evaluate(() => {
|
||||
const shown = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el !== null && !el.hidden && el.closest("[hidden]") === null && !el.disabled;
|
||||
};
|
||||
return {
|
||||
over: !document.getElementById("panel-veil").hidden || /\/bilan$/.test(location.hash),
|
||||
busy: document.body.classList.contains("busy"),
|
||||
take: shown("take"),
|
||||
confirm: shown("confirm"),
|
||||
skip: shown("skip"),
|
||||
roll: shown("roll"),
|
||||
land: document.querySelector("#board .hit.can-land") !== null,
|
||||
move: document.querySelector("#board .hit.can-move") !== null,
|
||||
};
|
||||
});
|
||||
if (s.over) {
|
||||
return;
|
||||
}
|
||||
if (s.busy) {
|
||||
await sleep(100);
|
||||
} else if (s.take) {
|
||||
await page.click("#take");
|
||||
} else if (s.confirm) {
|
||||
await page.click("#confirm");
|
||||
} else if (s.skip) {
|
||||
await page.click("#skip");
|
||||
} else if (s.land) {
|
||||
await page.locator("#board .hit.can-land").first().click();
|
||||
} else if (s.move) {
|
||||
await page.locator("#board .hit.can-move").first().click();
|
||||
} else if (s.roll) {
|
||||
await page.click("#roll");
|
||||
} else {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
check(!(await page.locator("#confirm").isHidden()), `coup incomplet : ${JSON.stringify(halfMoves)}`);
|
||||
await page.click("#confirm");
|
||||
throw new Error("la partie ne finit pas");
|
||||
}
|
||||
|
||||
/** Le bilan de la partie qui vient de finir : son identifiant et son taux
|
||||
@@ -178,17 +202,10 @@ async function summaryRate() {
|
||||
}));
|
||||
}
|
||||
|
||||
/** Une partie : le joueur joue 6/5/off (puis passe le double) ou 6/off
|
||||
* 1/off. */
|
||||
async function oneGame(mistake) {
|
||||
await startFrom(BEAROFF);
|
||||
if (mistake) {
|
||||
await play([6, 1], [[6, 5], [5, 0]]);
|
||||
await until("double de l'ordinateur", () => !document.getElementById("drop").hidden);
|
||||
await page.click("#drop");
|
||||
} else {
|
||||
await play([6, 1], [[6, 0], [1, 0]]);
|
||||
}
|
||||
/** Une course jouée jusqu'au bout, et le taux de son bilan. */
|
||||
async function oneGame() {
|
||||
await startFrom(RACE);
|
||||
await playOut();
|
||||
return summaryRate();
|
||||
}
|
||||
|
||||
@@ -228,14 +245,13 @@ log("progression sans partie terminée : phrase et « Nouvelle partie »");
|
||||
// ----------------------------------------------------- Trois parties jouées
|
||||
|
||||
const games = [];
|
||||
for (const mistake of [true, false, true]) {
|
||||
games.push(await oneGame(mistake));
|
||||
for (let i = 0; i < 3; i++) {
|
||||
games.push(await oneGame());
|
||||
}
|
||||
check(
|
||||
games.every((g) => g.id && g.rate !== undefined && !Number.isNaN(Number(g.rate))),
|
||||
`bilans : ${JSON.stringify(games)}`,
|
||||
);
|
||||
check(Number(games[0].rate) > 0 && Number(games[1].rate) === 0, `taux des bilans : ${JSON.stringify(games)}`);
|
||||
log(`trois parties jouées, taux des bilans : ${games.map((g) => g.rate).join(", ")}`);
|
||||
|
||||
// La ligne de l'accueil mène à la progression.
|
||||
@@ -251,9 +267,23 @@ await until("accueil", () => {
|
||||
});
|
||||
const line = await page.evaluate(() => {
|
||||
const a = document.getElementById("home-progress");
|
||||
return { tag: a.tagName, href: a.getAttribute("href"), text: a.textContent };
|
||||
return {
|
||||
tag: a.tagName,
|
||||
href: a.getAttribute("href"),
|
||||
text: a.textContent,
|
||||
height: a.getBoundingClientRect().height,
|
||||
// La carte de la partie, sans partie en cours : ni miniature ni
|
||||
// compte de la partie finie (recette 1h).
|
||||
mini: !document.querySelector("#home-game .home-mini").hidden,
|
||||
pips: !document.getElementById("home-pips").hidden,
|
||||
resume: !document.getElementById("home-resume").hidden,
|
||||
};
|
||||
});
|
||||
check(line.tag === "A" && line.href === "#/progression", `ligne de l'accueil : ${JSON.stringify(line)}`);
|
||||
check(
|
||||
line.tag === "A" && line.href === "#/progression" && line.height >= 44,
|
||||
`ligne de l'accueil : ${JSON.stringify(line)}`,
|
||||
);
|
||||
check(!line.mini && !line.pips && !line.resume, `carte de la partie finie : ${JSON.stringify(line)}`);
|
||||
await capture("3-accueil-ligne", async () => {
|
||||
await page.locator("#home-progress").scrollIntoViewIfNeeded();
|
||||
});
|
||||
@@ -269,22 +299,36 @@ const shown = await page.evaluate(() => ({
|
||||
id: c.getAttribute("data-id"),
|
||||
rate: c.getAttribute("data-rate"),
|
||||
last: c.classList.contains("last"),
|
||||
better: c.classList.contains("better"),
|
||||
r: Number(c.getAttribute("r")),
|
||||
})),
|
||||
values: [...document.querySelectorAll("#progression-curve .progression-value")].length,
|
||||
axisLabels: [...document.querySelectorAll("#progression-curve .curve-label")].length,
|
||||
items: [...document.querySelectorAll("#progression-list .progression-item")].map((li) => ({
|
||||
id: li.dataset.id,
|
||||
summary: li.querySelector(".progression-game").getAttribute("href"),
|
||||
analysis: li.querySelector(".progression-analysis").getAttribute("href"),
|
||||
links: li.querySelectorAll("a, button").length,
|
||||
chevron: li.querySelector(".progression-game > svg.icon") !== null,
|
||||
rate: li.querySelector(".progression-rate")?.dataset.rate,
|
||||
})),
|
||||
glossary: document.getElementById("progression-glossary")?.getAttribute("href"),
|
||||
exercises: document.getElementById("progression-exercises")?.getAttribute("href"),
|
||||
exercisesText: document.getElementById("progression-exercises")?.textContent,
|
||||
back: document.getElementById("progression-back") !== null,
|
||||
title: document.getElementById("progression-title")?.tagName,
|
||||
}));
|
||||
check(shown.dots.length === 3, `courbe : ${shown.dots.length} points`);
|
||||
for (const [i, game] of games.entries()) {
|
||||
const dot = shown.dots[i];
|
||||
check(dot.id === game.id && dot.rate === game.rate, `point ${i + 1} : ${JSON.stringify(dot)} contre ${JSON.stringify(game)}`);
|
||||
}
|
||||
check(shown.dots[2].last && !shown.dots[0].last, "le dernier point n'est pas mis en avant");
|
||||
check(shown.dots[2].last && !shown.dots[0].last && shown.dots[2].r > shown.dots[0].r,
|
||||
"le dernier point n'est pas plus gros");
|
||||
// Vert seulement si le taux baisse (progression.baisse).
|
||||
const down = shown.summary.includes("vous faites moins d'erreurs");
|
||||
check(shown.dots[2].better === down && !shown.dots[0].better, `vert du dernier point : ${shown.summary}`);
|
||||
check(shown.values === 2 && shown.axisLabels === 0, `valeurs de la courbe : ${shown.values}, axe : ${shown.axisLabels}`);
|
||||
check(!shown.back && shown.title === "H1", `en-tête : ${JSON.stringify({ back: shown.back, title: shown.title })}`);
|
||||
check(shown.summary.startsWith("Sur vos 3 dernières parties"), `phrase : ${shown.summary}`);
|
||||
check(!shown.summary.includes("!"), `phrase : ${shown.summary}`);
|
||||
check(shown.items.length === 3 && shown.items[0].id === games[2].id, `liste : ${JSON.stringify(shown.items)}`);
|
||||
@@ -292,13 +336,17 @@ for (const item of shown.items) {
|
||||
const game = games.find((g) => g.id === item.id);
|
||||
check(
|
||||
item.summary === `#/parties/${item.id}/bilan` &&
|
||||
item.analysis === `#/parties/${item.id}/analyse` &&
|
||||
item.links === 1 &&
|
||||
item.chevron &&
|
||||
item.rate === game.rate,
|
||||
`ligne : ${JSON.stringify(item)}`,
|
||||
);
|
||||
}
|
||||
check(shown.glossary === "#/glossaire/taux-d-erreur", `lien du glossaire : ${shown.glossary}`);
|
||||
check(shown.exercises === "#/progression/ia", `exercices : ${shown.exercises}`);
|
||||
check(
|
||||
shown.exercises === "#/progression/ia" && shown.exercisesText === "Demander des exercices à une IA",
|
||||
`exercices : ${shown.exercises} « ${shown.exercisesText} »`,
|
||||
);
|
||||
await capture("2-trois-parties");
|
||||
log(`courbe : ${shown.dots.map((d) => d.rate).join(", ")} ; « ${shown.summary} »`);
|
||||
|
||||
@@ -315,7 +363,7 @@ await page.click("#summary-close");
|
||||
await progressionReady();
|
||||
log("une ligne ouvre le bilan, la croix ramène à la progression");
|
||||
|
||||
// « Créer des exercices » : la feuille du prompt.
|
||||
// « Demander des exercices à une IA » : la feuille du prompt.
|
||||
await page.click("#progression-exercises");
|
||||
await until(
|
||||
"prompt des exercices",
|
||||
@@ -326,11 +374,11 @@ await until(
|
||||
const prompt = await page.textContent("#prompt-text");
|
||||
check(prompt.includes("Voici mes 3 dernières parties"), `prompt : ${prompt}`);
|
||||
check(prompt.includes("Ce qui revient dans ces erreurs"), `prompt : ${prompt}`);
|
||||
check(prompt.includes("Je débute au backgammon."), `prompt : ${prompt}`);
|
||||
check(/Je débute au backgammon\.|Je suis un joueur de niveau /.test(prompt), `prompt : ${prompt}`);
|
||||
check(!/[{}]/.test(prompt), `repère non rempli : ${prompt}`);
|
||||
await page.keyboard.press("Escape");
|
||||
await until("retour à la progression", () => location.hash === "#/progression");
|
||||
log("« Créer des exercices » : prompt avec les erreurs et leurs thèmes");
|
||||
log("« Demander des exercices à une IA » : prompt avec les erreurs et leurs thèmes");
|
||||
|
||||
const csp = await page.evaluate(() => window.__csp);
|
||||
for (const violation of csp) {
|
||||
|
||||
Reference in New Issue
Block a user