diff --git a/app/src/ui/analysis-view.ts b/app/src/ui/analysis-view.ts index 0766999..03827c1 100644 --- a/app/src/ui/analysis-view.ts +++ b/app/src/ui/analysis-view.ts @@ -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); diff --git a/app/src/ui/app.ts b/app/src/ui/app.ts index 0d1194f..6b0dea7 100644 --- a/app/src/ui/app.ts +++ b/app/src/ui/app.ts @@ -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)}`, }; } diff --git a/app/src/ui/summary-view.ts b/app/src/ui/summary-view.ts index 213352d..5c28288 100644 --- a/app/src/ui/summary-view.ts +++ b/app/src/ui/summary-view.ts @@ -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, diff --git a/app/src/ui/texts.ts b/app/src/ui/texts.ts index c73e28a..aab99c1 100644 --- a/app/src/ui/texts.ts +++ b/app/src/ui/texts.ts @@ -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"}${ diff --git a/app/tests/ui.test.ts b/app/tests/ui.test.ts index 48815ac..dc7a5e8 100644 --- a/app/tests/ui.test.ts +++ b/app/tests/ui.test.ts @@ -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.", diff --git a/docs/adr/0013-position-dans-l-adresse.md b/docs/adr/0013-position-dans-l-adresse.md index 7dfa5b1..e443000 100644 --- a/docs/adr/0013-position-dans-l-adresse.md +++ b/docs/adr/0013-position-dans-l-adresse.md @@ -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. diff --git a/tests/browser/bilan.mjs b/tests/browser/bilan.mjs index 976add5..0650864 100644 --- a/tests/browser/bilan.mjs +++ b/tests/browser/bilan.mjs @@ -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}`); diff --git a/tests/browser/match.mjs b/tests/browser/match.mjs index ce52877..b6b3796 100644 --- a/tests/browser/match.mjs +++ b/tests/browser/match.mjs @@ -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(); diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index ea064b3..52b774c 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -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`, diff --git a/tests/browser/recette.mjs b/tests/browser/recette.mjs index 66c66e4..a3d7b6f 100644 --- a/tests/browser/recette.mjs +++ b/tests/browser/recette.mjs @@ -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) : diff --git a/web/app.css b/web/app.css index 0056523..368b25c 100644 --- a/web/app.css +++ b/web/app.css @@ -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 {