Tests navigateur : accueil, tutoriel, aide, installation, mise à jour ; plan : résultat

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-09 15:35:00 +01:00
co-authored by Claude Opus 5.5
parent 441bca1bd7
commit 5fdca812d9
7 changed files with 644 additions and 30 deletions
+24 -11
View File
@@ -1289,13 +1289,21 @@ export function startApp(options: AppOptions): void {
// ----------------------------------------------------------- Adresses
let menuReturnFocus: HTMLElement | null = null;
// L'écran sous la feuille d'installation : le dernier écran affiché.
let underSheet: Route = { screen: "game" };
/** Dessine l'écran que désigne l'adresse (ADR 0013, amendement). */
function applyRoute(route: Route): void {
const help = isHelpScreen(route);
const position = route.screen === "position";
const home = route.screen === "home";
const tutorial = route.screen === "tutorial" || route.screen === "tutorialEnd";
// La feuille d'installation s'ouvre par-dessus l'écran d'où l'on
// vient (l'accueil, le plus souvent), qui reste dessous.
if (route.screen !== "install") {
underSheet = route;
}
const below = underSheet;
const help = isHelpScreen(below);
const position = below.screen === "position";
const home = below.screen === "home";
const tutorial = below.screen === "tutorial" || below.screen === "tutorialEnd";
const page = help || position || home || tutorial;
const wasPage = ui.game.hidden;
const wasScreen = ui.root.dataset.screen;
@@ -1307,12 +1315,14 @@ export function startApp(options: AppOptions): void {
ui.root.classList.toggle("page-open", page);
// Le tutoriel et le premier lancement ont leur propre en-tête.
ui.root.classList.toggle("bare", tutorial || (home && isFirstLaunch(settings)));
ui.root.dataset.screen = route.screen;
if ((wasPage && !ui.game.hidden) || (page && wasScreen !== route.screen)) {
ui.root.dataset.screen = below.screen;
if ((wasPage && !ui.game.hidden) || (page && wasScreen !== below.screen)) {
window.scrollTo(0, 0);
}
if (tutorial) {
void tutorialView.show(route);
if (route.screen !== "install") {
void tutorialView.show(below);
}
} else {
tutorialView.hide();
}
@@ -1361,11 +1371,14 @@ export function startApp(options: AppOptions): void {
gamesView?.hide();
}
if (help) {
void helpView.show(route);
const sheet = route.screen === "install";
if (isHelpScreen(below) && !sheet) {
void helpView.show(below);
}
if (position) {
void showPosition(route);
if (below.screen === "position") {
if (!sheet) {
void showPosition(below);
}
} else {
positionRequest++;
shownPosition = null;
+16 -8
View File
@@ -252,7 +252,13 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
// à l'écran (le joueur est déjà ailleurs).
let shown = 0;
let autoplay: ReturnType<typeof setTimeout> | null = null;
// Un coup illustré se joue, ou un geste est jugé : les touchers
// attendent (data-busy, pour les tests dans un navigateur).
let busy = false;
const setBusy = (value: boolean) => {
busy = value;
section.toggleAttribute("data-busy", value);
};
// L'écran affiché.
let screen: TutorialScreen | null = null;
@@ -349,7 +355,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
if (shownMove === null || id !== shown) {
return;
}
busy = true;
setBusy(true);
replay.disabled = true;
illustrated = false;
try {
@@ -376,7 +382,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
}
} finally {
if (id === shown) {
busy = false;
setBusy(false);
illustrated = true;
replay.disabled = false;
draw();
@@ -419,7 +425,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
return;
}
const id = shown;
busy = true;
setBusy(true);
selected = null;
try {
const verdict = await session.play(halfMove);
@@ -445,7 +451,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
await refreshTargets();
} finally {
if (id === shown) {
busy = false;
setBusy(false);
draw(practice === null ? null : await practice.board());
}
}
@@ -487,7 +493,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
return;
}
const id = shown;
busy = true;
setBusy(true);
try {
const right = (await access.shouldTake(current.position)) === takes;
if (id !== shown) {
@@ -498,7 +504,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
}
setVerdict(right ? "right" : "wrong", right ? current.right : current.wrong);
} finally {
busy = false;
setBusy(false);
}
}
take.addEventListener("click", () => void answer(true));
@@ -535,8 +541,10 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
return {
async show(route) {
const id = ++shown;
// Prêt (pour les tests dans un navigateur) une fois ce plateau dessiné.
delete section.dataset.ready;
clearTimer();
busy = false;
setBusy(false);
practice = null;
illustration = null;
illustrated = false;
@@ -696,7 +704,7 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
hide() {
shown++;
clearTimer();
busy = false;
setBusy(false);
},
};
}
+37
View File
@@ -6,6 +6,7 @@
// Licence : GPLv3 ou ultérieure (voir LICENSE).
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { installPlatform } from "../src/ui/install.js";
import {
@@ -15,7 +16,18 @@ import {
isFirstLaunch,
parseSettings,
type Settings,
TUTORIAL_DONE,
} from "../src/ui/settings.js";
import {
nextPlace,
placeOf,
previousPlace,
progress,
seenAfter,
} from "../src/ui/tutorial-nav.js";
import { parseTutorial } from "../src/ui/tutorial-text.js";
const root = new URL("../../../", import.meta.url);
test("réglages d'une version précédente : les nouveaux champs prennent leur valeur par défaut", () => {
const old = parseSettings('{"vibrations": false, "theme": "dark", "gamesStarted": 4}');
@@ -66,6 +78,31 @@ test("invitation à installer : après la première partie terminée, une seule
assert.ok(!at({ installDismissals: 2, installDismissedAt: 4, gamesFinished: 40 }));
});
test("tutoriel : écran suivant, précédent, adresse ramenée à un écran qui existe", () => {
const tutorial = parseTutorial(readFileSync(new URL("docs/tutoriel/regles.md", root), "utf8"));
const explain = (step: number) => ({ step, practice: false });
const practice = (step: number) => ({ step, practice: true });
assert.deepEqual(nextPlace(tutorial, explain(2)), explain(3));
assert.deepEqual(nextPlace(tutorial, explain(3)), practice(3));
assert.deepEqual(nextPlace(tutorial, practice(3)), explain(4));
assert.equal(nextPlace(tutorial, practice(8)), "end");
assert.equal(previousPlace(tutorial, explain(1)), null);
assert.deepEqual(previousPlace(tutorial, explain(4)), practice(3));
assert.deepEqual(previousPlace(tutorial, explain(3)), explain(2));
assert.deepEqual(previousPlace(tutorial, "end"), practice(8));
assert.deepEqual(placeOf(tutorial, { screen: "tutorial", step: null, practice: false }), explain(1));
assert.deepEqual(placeOf(tutorial, { screen: "tutorial", step: 2, practice: true }), explain(2));
assert.deepEqual(placeOf(tutorial, { screen: "tutorial", step: 42, practice: false }), explain(1));
assert.equal(placeOf(tutorial, { screen: "tutorialEnd" }), "end");
assert.equal(seenAfter(5, explain(3)), 5);
assert.equal(seenAfter(0, "end"), TUTORIAL_DONE);
// La barre : des liens vers les étapes vues seulement, pas l'actuelle.
const bar = progress(8, explain(3), 5);
assert.deepEqual(bar.filter((s) => s.link).map((s) => s.step), [1, 2, 4, 5]);
assert.deepEqual(bar.filter((s) => s.done).map((s) => s.step), [1, 2, 3]);
assert.deepEqual(bar.filter((s) => s.current).map((s) => s.step), [3]);
});
test("la marche à suivre pour installer dépend du navigateur", () => {
const iphone =
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 " +