Recette 1e : titre du bilan sur l'issue, notation du moteur partout, boutons de 48 px en paysage, filtre en accent-soft, longue partie ; tests navigateur adaptés au bilan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ced004edb0
commit
663ef35b51
@@ -446,7 +446,9 @@ export function createAnalysisView(options: AnalysisViewOptions): AnalysisView {
|
||||
? REVIEW_TEXTS.computerCube
|
||||
: turn.move.halfMoves.length === 0
|
||||
? REVIEW_TEXTS.computerCannotMove
|
||||
: REVIEW_TEXTS.computerPlayed(formatHalfMoves(turn.move.halfMoves));
|
||||
: REVIEW_TEXTS.computerPlayed(
|
||||
theirs?.played.notation ?? formatHalfMoves(turn.move.halfMoves),
|
||||
);
|
||||
} else if (turn.move !== null && turn.move.halfMoves.length === 0) {
|
||||
phrase.textContent = REVIEW_TEXTS.playerCannotMove;
|
||||
}
|
||||
@@ -456,6 +458,10 @@ export function createAnalysisView(options: AnalysisViewOptions): AnalysisView {
|
||||
const side = turn.side;
|
||||
let mine: MoveSteps | null = null;
|
||||
let best: MoveSteps | null = null;
|
||||
// Les coups s'écrivent dans la notation du moteur, comme dans le
|
||||
// bilan et la feuille du maître (« 6/off », pas « 6/5 5/off ») ;
|
||||
// à défaut d'analyse, à partir des demi-coups.
|
||||
let names = { mine: "", best: "" };
|
||||
const moveAdvice: Advice | null = side === "player" ? main : theirs;
|
||||
try {
|
||||
if (moveAdvice?.kind === "move") {
|
||||
@@ -464,6 +470,10 @@ export function createAnalysisView(options: AnalysisViewOptions): AnalysisView {
|
||||
side,
|
||||
moveAdvice.played.halfMoves,
|
||||
);
|
||||
names = {
|
||||
mine: moveAdvice.played.notation,
|
||||
best: moveAdvice.best.notation,
|
||||
};
|
||||
if (moveAdvice.rank > 1) {
|
||||
best = await current.game.stepsBefore(
|
||||
moveAdvice.eventId,
|
||||
@@ -475,6 +485,7 @@ export function createAnalysisView(options: AnalysisViewOptions): AnalysisView {
|
||||
mine = await current.game.stepsBefore(moveAdvice.eventId, "player", []);
|
||||
} else if (turn.move !== null) {
|
||||
mine = await current.game.stepsBefore(turn.move.id, side, turn.move.halfMoves);
|
||||
names = { mine: formatHalfMoves(turn.move.halfMoves), best: "" };
|
||||
} else {
|
||||
const first = turn.events[0];
|
||||
mine =
|
||||
@@ -488,15 +499,19 @@ export function createAnalysisView(options: AnalysisViewOptions): AnalysisView {
|
||||
if (id !== request || mine === null) {
|
||||
return;
|
||||
}
|
||||
showMoves(mine, best, side);
|
||||
showMoves(mine, best, side, names);
|
||||
})();
|
||||
}
|
||||
|
||||
function showMoves(mine: MoveSteps, best: MoveSteps | null, side: "player" | "computer"): void {
|
||||
function showMoves(
|
||||
mine: MoveSteps,
|
||||
best: MoveSteps | null,
|
||||
side: "player" | "computer",
|
||||
names: { readonly mine: string; readonly best: string },
|
||||
): void {
|
||||
const who = side === "player" ? REVIEW_TEXTS.yourMoveSegment : REVIEW_TEXTS.theirMove;
|
||||
segments.mine.textContent = who;
|
||||
choice.hidden = best === null;
|
||||
const notation = (steps: MoveSteps) => formatHalfMoves(steps.halfMoves);
|
||||
const select = (shown: Shown) => {
|
||||
const steps = shown === "best" && best !== null ? best : mine;
|
||||
for (const key of ["mine", "best"] as const) {
|
||||
@@ -511,7 +526,7 @@ export function createAnalysisView(options: AnalysisViewOptions): AnalysisView {
|
||||
}
|
||||
const text = REVIEW_TEXTS.legendMove(
|
||||
shown === "best" ? REVIEW_TEXTS.masterMove : who,
|
||||
notation(steps),
|
||||
shown === "best" ? names.best : names.mine,
|
||||
);
|
||||
legend.textContent = text;
|
||||
board.setAttribute("aria-label", text);
|
||||
|
||||
+6
-5
@@ -1419,7 +1419,7 @@ export function startApp(options: AppOptions): void {
|
||||
) {
|
||||
queueMicrotask(() => {
|
||||
if (navigation.route.screen === "game" && !panelDismissed) {
|
||||
navigation.replace(routeHash({ screen: "summary", game: id }));
|
||||
navigation.go(routeHash({ screen: "summary", game: id }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2527,6 +2527,7 @@ function endParts(game: Game): { title: string; detail: string } {
|
||||
}
|
||||
const { winner, kind, points } = result;
|
||||
const dropped = game.events.some((e) => e.type === "CubeDropped");
|
||||
const how = dropped ? TEXTS.droppedDouble(winner) : "";
|
||||
const { match, matchAfter } = game.state;
|
||||
if (match !== null && matchAfter !== null) {
|
||||
const { player, computer } = matchAfter.score;
|
||||
@@ -2541,13 +2542,13 @@ function endParts(game: Game): { title: string; detail: string } {
|
||||
return {
|
||||
title: matchAfter.over
|
||||
? MATCH_TEXTS.matchWonTitle(winner)
|
||||
: TEXTS.endTitle(winner, kind, dropped),
|
||||
detail: matchAfter.crawford ? `${detail} ${MATCH_TEXTS.crawfordNext}` : detail,
|
||||
: TEXTS.endTitle(winner, kind),
|
||||
detail: `${how}${matchAfter.crawford ? `${detail} ${MATCH_TEXTS.crawfordNext}` : detail}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: TEXTS.endTitle(winner, kind, dropped),
|
||||
detail: TEXTS.endDetail(winner, points, game.state.cube.value),
|
||||
title: TEXTS.endTitle(winner, kind),
|
||||
detail: `${how}${TEXTS.endDetail(winner, points, game.state.cube.value)}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,10 @@ export function createSummaryView(options: SummaryViewOptions): SummaryView {
|
||||
}
|
||||
|
||||
function actions(data: SummaryData): void {
|
||||
const bar = element(panel, "div", { className: "panel-actions summary-actions" });
|
||||
// Les actions et le lien des réglages, ensemble : en paysage, en bas
|
||||
// de la colonne de droite.
|
||||
const end = element(panel, "div", { className: "summary-end" });
|
||||
const bar = element(end, "div", { className: "panel-actions summary-actions" });
|
||||
const analysis = element(bar, "a", {
|
||||
id: "summary-analysis",
|
||||
className: "button secondary",
|
||||
@@ -304,7 +307,7 @@ export function createSummaryView(options: SummaryViewOptions): SummaryView {
|
||||
});
|
||||
again.type = "button";
|
||||
again.onclick = () => options.again();
|
||||
const settings = element(panel, "a", {
|
||||
const settings = element(end, "a", {
|
||||
id: "summary-settings",
|
||||
className: "panel-link",
|
||||
text: MATCH_TEXTS.changeSettings,
|
||||
|
||||
+11
-8
@@ -136,15 +136,18 @@ export const TEXTS = {
|
||||
// Fin de partie
|
||||
gameOver: "Partie finie",
|
||||
endOverline: "Fin de partie",
|
||||
/** Titre du panneau de fin : qui gagne, et comment. */
|
||||
endTitle: (winner: "player" | "computer", kind: ResultKind, dropped: boolean) => {
|
||||
if (dropped) {
|
||||
return winner === "player" ? "L'ordinateur passe" : "Vous passez";
|
||||
}
|
||||
return winner === "player"
|
||||
/** Titre du panneau de fin et du bilan : toujours l'issue, qui gagne
|
||||
* et comment (un double passé se dit dans le détail). */
|
||||
endTitle: (winner: "player" | "computer", kind: ResultKind) =>
|
||||
winner === "player"
|
||||
? `Vous gagnez ${RESULT_KIND[kind]}`
|
||||
: `L'ordinateur gagne ${RESULT_KIND[kind]}`;
|
||||
},
|
||||
: `L'ordinateur gagne ${RESULT_KIND[kind]}`,
|
||||
/** Avant le détail des points, quand la partie finit sur un double
|
||||
* passé : « Vous avez passé son double : 1 point pour lui. ». */
|
||||
droppedDouble: (winner: "player" | "computer") =>
|
||||
winner === "player"
|
||||
? "L'ordinateur a passé votre double : "
|
||||
: "Vous avez passé son double : ",
|
||||
/** « 8 points pour lui, videau à 4. » */
|
||||
endDetail: (winner: "player" | "computer", n: number, cube: number) =>
|
||||
`${points(n)} pour ${winner === "player" ? "vous" : "lui"}${
|
||||
|
||||
@@ -242,11 +242,15 @@ test("l'aide lit chaque fiche de l'index du glossaire", () => {
|
||||
});
|
||||
|
||||
test("les textes de fin de partie emploient les mots du glossaire", () => {
|
||||
assert.equal(TEXTS.endTitle("computer", "gammon", false), "L'ordinateur gagne un gammon");
|
||||
assert.equal(TEXTS.endTitle("computer", "gammon"), "L'ordinateur gagne un gammon");
|
||||
assert.equal(TEXTS.endDetail("computer", 8, 4), "8 points pour lui, videau à 4.");
|
||||
assert.equal(TEXTS.endTitle("player", "single", false), "Vous gagnez la partie simple");
|
||||
assert.equal(TEXTS.endTitle("player", "single"), "Vous gagnez la partie simple");
|
||||
assert.equal(TEXTS.endDetail("player", 1, 1), "1 point pour vous.");
|
||||
assert.equal(TEXTS.endTitle("player", "single", true), "L'ordinateur passe");
|
||||
// Un double passé : le titre donne l'issue, le détail dit comment.
|
||||
assert.equal(
|
||||
`${TEXTS.droppedDouble("computer")}${TEXTS.endDetail("computer", 1, 1)}`,
|
||||
"Vous avez passé son double : 1 point pour lui.",
|
||||
);
|
||||
assert.equal(
|
||||
TEXTS.computerMoved([6, 4], "24/18 13/9"),
|
||||
"L'ordinateur a joué 6-4 : 24/18 13/9.",
|
||||
|
||||
@@ -147,8 +147,8 @@ tour de la partie (ce qu'un camp a fait quand il avait la main : doubler ou
|
||||
non, lancer, jouer), à partir de 1, sur sa ligne courante.
|
||||
|
||||
- Le bilan remplace le panneau de fin de partie dès que l'analyse est
|
||||
prête : l'adresse de la partie (`#/`) est alors remplacée par celle du
|
||||
bilan, sans rien empiler. Il monte au-dessus de la partie, comme une
|
||||
prête : l'application va à l'adresse du bilan, par-dessus celle de la
|
||||
partie (`#/`). Il monte au-dessus de la partie, comme une
|
||||
feuille ; le fermer (croix, retour du téléphone) laisse voir la partie
|
||||
finie, sans le panneau. « Mes parties » donne le bilan de chaque partie
|
||||
finie.
|
||||
|
||||
+102
-4
@@ -22,7 +22,7 @@
|
||||
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { chromium, firefox } from "playwright";
|
||||
import { hiddenUnderActionBar } from "./recette.mjs";
|
||||
import { hiddenUnderActionBar, tallButtons } from "./recette.mjs";
|
||||
|
||||
const [base, browserName, shots] = process.argv.slice(2);
|
||||
const browserType = { chromium, firefox }[browserName];
|
||||
@@ -116,6 +116,8 @@ async function capture(name) {
|
||||
hidden.length === 0,
|
||||
`${name} ${size.width}x${size.height} : caché sous la barre d'action : ${hidden.join(", ")}`,
|
||||
);
|
||||
const tall = await tallButtons(page);
|
||||
check(tall.length === 0, `${name} ${size.width}x${size.height} : boutons étirés : ${tall.join(", ")}`);
|
||||
await page.screenshot({ path: `${dir}/${name}-${size.width}x${size.height}.png` });
|
||||
captures++;
|
||||
}
|
||||
@@ -172,8 +174,19 @@ async function play(dice, halfMoves) {
|
||||
|
||||
await startFrom(BEAROFF);
|
||||
await play([6, 1], [[6, 5], [5, 0]]);
|
||||
// L'ordinateur, sûr de gagner, double : le joueur passe.
|
||||
// L'ordinateur, sûr de gagner, double. La ligne du maître commente le
|
||||
// coup : sa feuille d'explication écrit le coup joué dans la notation du
|
||||
// moteur, comme le bilan et l'analyse plus bas.
|
||||
await until("double de l'ordinateur", () => !document.getElementById("drop").hidden);
|
||||
await until("ligne du maître", () => !document.getElementById("advice-line").hidden);
|
||||
await page.click("#advice-line");
|
||||
await until("feuille du maître", () => document.getElementById("explain-mine") !== null);
|
||||
await page.click("#explain-mine");
|
||||
const sheetLegend = await page.textContent("#explain-legend");
|
||||
check(sheetLegend.includes(" 6/off,"), `feuille du maître : ${sheetLegend}`);
|
||||
await page.click("#explain-done");
|
||||
await until("feuille fermée", () => document.getElementById("explain-veil").hidden);
|
||||
// Le joueur passe.
|
||||
await page.click("#drop");
|
||||
// Le panneau de fin de partie, puis le bilan à sa place, à son adresse.
|
||||
await until(
|
||||
@@ -203,8 +216,11 @@ const summary = await page.evaluate(() => ({
|
||||
}));
|
||||
const id = summary.hash.split("/")[2];
|
||||
check(summary.panelHidden, "le panneau de fin de partie reste sous le bilan");
|
||||
check(summary.title === "Vous passez", `titre : ${summary.title}`);
|
||||
check(summary.reading.startsWith("1 point pour lui."), `lecture : ${summary.reading}`);
|
||||
check(summary.title === "L'ordinateur gagne la partie simple", `titre : ${summary.title}`);
|
||||
check(
|
||||
summary.reading.startsWith("Vous avez passé son double : 1 point pour lui. "),
|
||||
`lecture : ${summary.reading}`,
|
||||
);
|
||||
check(summary.reading.includes("D'après mon analyse"), `lecture : ${summary.reading}`);
|
||||
check(!summary.reading.includes("!"), `lecture : ${summary.reading}`);
|
||||
// Le refus du double, juste ; le coup 6/5/off, grosse erreur ; le jet
|
||||
@@ -309,6 +325,19 @@ const best = await page.evaluate(() => ({
|
||||
move: document.getElementById("analysis-board").dataset.move,
|
||||
}));
|
||||
check(best.move === "best" && best.legend.includes("6/off 1/off"), `maître : ${best.legend}`);
|
||||
// Une seule notation, celle du moteur : le même coup s'écrit pareil dans
|
||||
// la feuille du maître, le bilan, la liste et le visualiseur.
|
||||
await page.click("#analysis-mine");
|
||||
const notations = await page.evaluate(() => ({
|
||||
legend: document.getElementById("analysis-legend").textContent,
|
||||
row: document.querySelector('#analysis-list .analysis-row[data-turn="1"] .row-move')
|
||||
?.textContent,
|
||||
}));
|
||||
check(
|
||||
notations.legend.includes(" 6/off,") && notations.row === "6/off",
|
||||
`notation : visualiseur « ${notations.legend} », liste « ${notations.row} »`,
|
||||
);
|
||||
await page.click("#analysis-best");
|
||||
await capture("5-visualiseur");
|
||||
log("analyse : courbe, point touché, clavier, liste filtrée, visualiseur");
|
||||
|
||||
@@ -370,6 +399,75 @@ check(branches.length === 2, `branches : ${branches.join(" | ")}`);
|
||||
check(branches[0].startsWith("Partie d'origine : 1 coup, finie"), `origine : ${branches[0]}`);
|
||||
log(`rejouer d'ici : ${branches.join(" | ")}`);
|
||||
|
||||
// ------------------------------------------- Une partie plus longue
|
||||
|
||||
// Pour juger la courbe en usage réel (Chromium seulement) : une longue
|
||||
// course (tests/reference/positions.json, race-2) jouée jusqu'au bout,
|
||||
// le joueur prenant à chaque fois le premier pion jouable et sa première
|
||||
// arrivée, puis son analyse complète.
|
||||
if (full) {
|
||||
await startFrom("cLezAQBwN2cGAA");
|
||||
for (let actions = 0; ; actions++) {
|
||||
check(actions < 2000, "la longue partie ne finit pas");
|
||||
const what = await page.evaluate(() => {
|
||||
if (document.body.classList.contains("busy")) {
|
||||
return null;
|
||||
}
|
||||
if (!document.getElementById("summary-veil").hidden) {
|
||||
return "over";
|
||||
}
|
||||
if (!document.getElementById("panel-veil").hidden) {
|
||||
return "panel";
|
||||
}
|
||||
const usable = (id) => {
|
||||
const b = document.getElementById(id);
|
||||
return !b.hidden && !b.disabled;
|
||||
};
|
||||
for (const id of ["take", "skip", "confirm", "roll"]) {
|
||||
if (usable(id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
if (document.querySelector("#board .hit.can-land")) {
|
||||
return "land";
|
||||
}
|
||||
return document.querySelector("#board .hit.can-move") ? "move" : null;
|
||||
});
|
||||
if (what === "over") {
|
||||
break;
|
||||
}
|
||||
if (what === null || what === "panel") {
|
||||
await sleep(100);
|
||||
continue;
|
||||
}
|
||||
const target = {
|
||||
take: "#take",
|
||||
skip: "#skip",
|
||||
confirm: "#confirm",
|
||||
roll: "#roll",
|
||||
land: "#board .hit.can-land",
|
||||
move: "#board .hit.can-move",
|
||||
}[what];
|
||||
await page.locator(target).first().click();
|
||||
await idle();
|
||||
}
|
||||
await until("bilan de la longue partie", () =>
|
||||
document.getElementById("summary")?.dataset.ready === "true");
|
||||
await page.click("#summary-analysis");
|
||||
await until("analyse complète de la longue partie", () => {
|
||||
const bands = document.querySelectorAll("#analysis-curve .curve-band").length;
|
||||
return (
|
||||
!document.getElementById("analysis").hidden &&
|
||||
bands >= 20 &&
|
||||
document.querySelectorAll("#analysis-curve .curve-dot").length === bands
|
||||
);
|
||||
}, null, 600_000);
|
||||
const turns = await page.locator("#analysis-curve .curve-band").count();
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await capture("7-analyse-partie-longue");
|
||||
log(`longue partie : ${turns} coups, courbe complète`);
|
||||
}
|
||||
|
||||
const csp = await page.evaluate(() => window.__csp);
|
||||
for (const violation of csp) {
|
||||
problems.push(`politique de sécurité : ${violation}`);
|
||||
|
||||
+19
-6
@@ -117,6 +117,10 @@ async function todo() {
|
||||
if (!panel.hidden) {
|
||||
return panel.dataset.kind;
|
||||
}
|
||||
// Le bilan remplace le panneau de fin de partie (étape 1e).
|
||||
if (!document.getElementById("summary-veil").hidden) {
|
||||
return "over";
|
||||
}
|
||||
const usable = (id) => {
|
||||
const b = document.getElementById(id);
|
||||
return !b.hidden && !b.disabled;
|
||||
@@ -282,12 +286,16 @@ for (;;) {
|
||||
check(now.cube === 0 && !now.double, `partie Crawford : videau visible ${JSON.stringify(now)}`);
|
||||
}
|
||||
if (what === "over") {
|
||||
const title = await page.textContent("#panel-text");
|
||||
const detail = await page.textContent("#panel-detail");
|
||||
const primary = await page.textContent("#panel-primary");
|
||||
// Le bilan, une fois l'analyse prête, à la place du panneau.
|
||||
await until("bilan prêt", () =>
|
||||
document.getElementById("summary")?.dataset.ready === "true", null, 120_000);
|
||||
const title = await page.textContent("#summary-title");
|
||||
const reading = await page.textContent("#summary-reading");
|
||||
const detail = reading.split(" D'après mon analyse")[0];
|
||||
const primary = await page.textContent("#summary-primary");
|
||||
log(`partie ${games}${crawford ? " (Crawford)" : ""} : ${title} ; ${detail}`);
|
||||
const settingsLink = await page.evaluate(() => {
|
||||
const a = document.getElementById("panel-settings");
|
||||
const a = document.getElementById("summary-settings");
|
||||
return a.hidden ? null : { text: a.textContent, href: a.getAttribute("href") };
|
||||
});
|
||||
check(settingsLink?.text === "Changer les réglages" && settingsLink.href === "#/nouvelle-partie",
|
||||
@@ -309,10 +317,15 @@ for (;;) {
|
||||
// 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 page.click("#summary-settings");
|
||||
await until("écran « Nouvelle partie » après le match", () =>
|
||||
location.hash === "#/nouvelle-partie" && document.getElementById("panel-veil").hidden);
|
||||
await page.click("#new-game-close");
|
||||
// Retour au bilan ; le fermer laisse le résultat sur le plateau.
|
||||
await until("retour au bilan du match", () =>
|
||||
/\/bilan$/.test(location.hash) &&
|
||||
document.getElementById("summary")?.dataset.ready === "true");
|
||||
await page.click("#summary-close");
|
||||
await until("retour au résultat du match", () => location.hash === "#/" &&
|
||||
!document.getElementById("again").hidden);
|
||||
check((await page.textContent("#again")) === "Rejouer",
|
||||
@@ -334,7 +347,7 @@ for (;;) {
|
||||
}
|
||||
check(primary === "Partie suivante" && /Match en 3 points : vous \d+, l'ordinateur \d+/.test(detail),
|
||||
`fin de partie dans un match : ${primary} ; ${detail}`);
|
||||
await page.click("#panel-primary");
|
||||
await page.click("#summary-primary");
|
||||
games++;
|
||||
await until("partie suivante", () => !document.body.classList.contains("busy"));
|
||||
const next = await shownMatch();
|
||||
|
||||
+46
-21
@@ -184,13 +184,21 @@ async function savedGameCount() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Les événements d'un journal et son événement courant : la partie,
|
||||
* sans les analyses du moteur qui s'y rangent en arrière-plan (conseils,
|
||||
* analyse de partie, étapes 1d et 1e). */
|
||||
function played(journal) {
|
||||
return journal === null ? null : { events: journal.events, head: journal.head };
|
||||
}
|
||||
|
||||
/** Le journal enregistré, une fois les enregistrements en cours terminés
|
||||
* (l'application enregistre sans attendre) : deux lectures identiques. */
|
||||
* (l'application enregistre sans attendre) : deux lectures identiques de
|
||||
* la partie. */
|
||||
async function savedJournal() {
|
||||
let last = await readJournal();
|
||||
let last = played(await readJournal());
|
||||
for (let i = 0; i < 50; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
const next = await readJournal();
|
||||
const next = played(await readJournal());
|
||||
if (JSON.stringify(next) === JSON.stringify(last)) {
|
||||
return next;
|
||||
}
|
||||
@@ -222,6 +230,10 @@ async function waitForPlayer() {
|
||||
if (!panel.hidden) {
|
||||
return panel.dataset.kind;
|
||||
}
|
||||
// Le bilan remplace le panneau de fin de partie (étape 1e).
|
||||
if (!document.getElementById("summary-veil").hidden) {
|
||||
return "over";
|
||||
}
|
||||
const usable = (id) => {
|
||||
const b = document.getElementById(id);
|
||||
return !b.hidden && !b.disabled;
|
||||
@@ -1523,13 +1535,23 @@ for (let actions = 0; ; actions++) {
|
||||
|
||||
// --------------------------------------------------------- Fin de partie
|
||||
|
||||
// Le panneau de fin de partie, puis le bilan à sa place une fois
|
||||
// l'analyse prête (étape 1e, testé en détail par bilan.mjs).
|
||||
await page.waitForSelector('#summary[data-ready="true"]', { timeout: 120_000 });
|
||||
const journal = await savedJournal();
|
||||
const result = `${await page.textContent("#panel-text")}, ${await page.textContent("#panel-detail")}`;
|
||||
check((await page.textContent("#panel-overline")) === "Fin de partie" &&
|
||||
/^(Vous gagnez|L'ordinateur gagne|Vous passez|L'ordinateur passe)/.test(
|
||||
await page.textContent("#panel-text")) &&
|
||||
/^\d+ points? pour (vous|lui)(, videau à \d+)?\.$/.test(await page.textContent("#panel-detail")),
|
||||
`panneau de fin de partie : ${result}`);
|
||||
/** Le titre du bilan et la phrase du résultat. */
|
||||
async function summaryResult() {
|
||||
const title = await page.textContent("#summary-title");
|
||||
const reading = await page.textContent("#summary-reading");
|
||||
const detail = /^(Vous avez passé son double : |L'ordinateur a passé votre double : )?\d+ points? pour (vous|lui)(, videau à \d+)?\./.exec(reading)?.[0] ?? reading;
|
||||
return `${title}, ${detail}`;
|
||||
}
|
||||
const result = await summaryResult();
|
||||
check((await page.textContent("#summary .overline")) === "Fin de partie" &&
|
||||
/^(Vous gagnez|L'ordinateur gagne)/.test(await page.textContent("#summary-title")) &&
|
||||
/^(Vous avez passé son double : |L'ordinateur a passé votre double : )?\d+ points? pour (vous|lui)(, videau à \d+)?\. /.test(
|
||||
await page.textContent("#summary-reading")),
|
||||
`bilan de fin de partie : ${result}`);
|
||||
check((await page.textContent("#prompt")) === "", "fin de partie : la consigne répète le panneau");
|
||||
log(
|
||||
`partie finie : ${result} (${countMoves(journal, "player")} coups du joueur, ` +
|
||||
@@ -1538,11 +1560,11 @@ log(
|
||||
const won = journal.events.at(-1).winner === "player";
|
||||
check((await vibrations()).at(-1) === (won ? "30,50,30,50,60" : "60"),
|
||||
`fin de partie : vibration ${(await vibrations()).at(-1)}`);
|
||||
check((await page.textContent("#panel-secondary")) === "Revoir la partie" &&
|
||||
(await page.textContent("#panel-primary")) === "Rejouer" &&
|
||||
(await page.textContent("#panel-settings")) === "Changer les réglages" &&
|
||||
(await page.getAttribute("#panel-settings", "href")) === "#/nouvelle-partie",
|
||||
"fin de partie : panneau sans « Revoir la partie », « Rejouer » et « Changer les réglages »");
|
||||
check((await page.textContent("#summary-analysis")) === "Voir l'analyse" &&
|
||||
(await page.textContent("#summary-primary")) === "Rejouer" &&
|
||||
(await page.textContent("#summary-settings")) === "Changer les réglages" &&
|
||||
(await page.getAttribute("#summary-settings", "href")) === "#/nouvelle-partie",
|
||||
"fin de partie : bilan sans « Voir l'analyse », « Rejouer » et « Changer les réglages »");
|
||||
await review("6-fin-de-partie");
|
||||
await page.setViewportSize({ width: 844, height: 320 });
|
||||
await sleep(150);
|
||||
@@ -1573,12 +1595,13 @@ log(skips > 0 ? `aucun coup possible ${skips} fois (${skipsByDice} en touchant l
|
||||
log(`vibrations demandées : ${(await vibrations()).length}`);
|
||||
await shot("4-fin-de-partie");
|
||||
|
||||
// « Revoir la partie » ferme le panneau ; « Rejouer » reste dans la barre
|
||||
// d'actions.
|
||||
await page.click("#panel-secondary");
|
||||
// Fermer le bilan laisse voir la partie finie, sans panneau ; « Rejouer »
|
||||
// reste dans la barre d'actions.
|
||||
await page.click("#summary-close");
|
||||
await page.waitForSelector("#summary-veil", { state: "hidden" });
|
||||
check(!(await page.isVisible("#panel")) && (await page.textContent("#again")) === "Rejouer" &&
|
||||
(await page.isVisible("#again")),
|
||||
"« Revoir la partie » : panneau toujours là, ou pas de « Rejouer »");
|
||||
"bilan fermé : panneau toujours là, ou pas de « Rejouer »");
|
||||
|
||||
step("mes parties");
|
||||
// Mes parties (IndexedDB), depuis le menu : une deuxième partie commencée,
|
||||
@@ -1628,14 +1651,16 @@ step("mes parties");
|
||||
}));
|
||||
throw new Error(`reprise bloquée : ${JSON.stringify(state)}`);
|
||||
});
|
||||
await page.waitForSelector("#panel");
|
||||
// La partie finie reprise : son bilan, déjà analysé, à la place du
|
||||
// panneau.
|
||||
await page.waitForSelector('#summary[data-ready="true"]', { timeout: 60_000 });
|
||||
const resumed = await savedJournal();
|
||||
check(JSON.stringify(resumed) === JSON.stringify(finished), "la première partie n'a pas été reprise");
|
||||
const shown = `${await page.textContent("#panel-text")}, ${await page.textContent("#panel-detail")}`;
|
||||
const shown = await summaryResult();
|
||||
check(shown === result, `écran différent après la reprise : ${shown}`);
|
||||
check((await page.evaluate(() => performance.getEntriesByType("navigation").length)) === loads,
|
||||
"la page a été rechargée");
|
||||
await page.click("#panel-secondary");
|
||||
await page.click("#summary-close");
|
||||
log(
|
||||
`mes parties : ${listed} parties listées (${branchCount} branche(s) pour la première), ` +
|
||||
`SGF exporté (${sgf.length} octets), première partie reprise sans rechargement`,
|
||||
|
||||
@@ -5,6 +5,32 @@
|
||||
// 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) :
|
||||
|
||||
+19
-5
@@ -3116,8 +3116,14 @@ html.landscape .explain-sheet > .explain-body {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Deux boutons de 48 px, côte à côte, jamais étirés en hauteur. */
|
||||
.summary-actions {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-actions > * {
|
||||
flex: 1 1 0;
|
||||
height: var(--button-height);
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
@@ -3137,8 +3143,7 @@ html.landscape .summary-panel > * {
|
||||
}
|
||||
|
||||
html.landscape .summary-panel > .summary-review,
|
||||
html.landscape .summary-panel > .summary-actions,
|
||||
html.landscape .summary-panel > .panel-link {
|
||||
html.landscape .summary-panel > .summary-end {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
@@ -3148,6 +3153,13 @@ html.landscape .summary-panel > .summary-review {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
/* Les actions en bas de la colonne de droite, « Changer les réglages »
|
||||
* dessous, comme en portrait. */
|
||||
html.landscape .summary-panel > .summary-end {
|
||||
grid-row: 5 / span 2;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* ------------------------------------------- Analyse d'une partie (1e) */
|
||||
|
||||
/* Maquette interactive « Analyse » : une page qui défile. */
|
||||
@@ -3308,10 +3320,12 @@ html.landscape .summary-panel > .summary-review {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Le filtre choisi en accent-soft, comme l'onglet choisi de l'aide. */
|
||||
.filters .filter[aria-pressed="true"] {
|
||||
color: var(--surface);
|
||||
background: var(--text);
|
||||
border-color: var(--text);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
background: var(--accent-soft);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.analysis-list {
|
||||
|
||||
Reference in New Issue
Block a user