// Tests du routeur (app/src/ui/router.ts) : chaque adresse du tableau de // l'ADR 0013 (amendement du 2026-11-03) mène au bon écran, et chaque écran // redonne son adresse. // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). import assert from "node:assert/strict"; import { test } from "node:test"; import { isAiRoute, isHelpScreen, parseRoute, type Route, routeHash, slug, stepsBack, withoutAi, } from "../src/ui/router.js"; const START = "4HPwATDgc/ABMA"; const MATCH = "cAkAAAAAAAAA"; /** Les adresses du tableau de l'ADR, et l'écran de chacune. */ const TABLE: [string, Route][] = [ ["#/", { screen: "game" }], ["#/accueil", { screen: "home" }], ["#/aide", { screen: "help", question: null }], ["#/aide/comment-jouer", { screen: "help", question: "comment-jouer" }], ["#/glossaire", { screen: "glossary", term: null }], ["#/glossaire/videau", { screen: "glossary", term: "videau" }], ["#/a-propos", { screen: "about" }], ["#/tutoriel", { screen: "tutorial", step: null, practice: false }], ["#/tutoriel/3", { screen: "tutorial", step: 3, practice: false }], ["#/tutoriel/3/a-vous", { screen: "tutorial", step: 3, practice: true }], ["#/tutoriel/fin", { screen: "tutorialEnd" }], // Les leçons (amendement du 2026-11-12, 1g). ["#/lecons", { screen: "lessons" }], ["#/lecons/2", { screen: "lesson", lesson: 2, step: null, practice: false }], ["#/lecons/2/3", { screen: "lesson", lesson: 2, step: 3, practice: false }], ["#/lecons/2/3/a-vous", { screen: "lesson", lesson: 2, step: 3, practice: true }], ["#/lecons/2/fin", { screen: "lessonEnd", lesson: 2 }], ["#/installer", { screen: "install" }], ["#/parties", { screen: "games", game: null }], ["#/parties/mh2k3x-a1b2c3", { screen: "games", game: "mh2k3x-a1b2c3" }], ["#/parties/mh2k3x-a1b2c3/bilan", { screen: "summary", game: "mh2k3x-a1b2c3" }], ["#/parties/mh2k3x-a1b2c3/analyse", { screen: "analysis", game: "mh2k3x-a1b2c3", turn: null }], ["#/parties/mh2k3x-a1b2c3/analyse/7", { screen: "analysis", game: "mh2k3x-a1b2c3", turn: 7 }], // La progression (étape 1h) et son prompt « Créer des exercices ». ["#/progression", { screen: "progression" }], ["#/progression/ia", { screen: "progression", ai: true }], ["#/menu", { screen: "menu" }], ["#/nouvelle-partie", { screen: "newGame" }], ["#/explication/12", { screen: "explanation", event: "12" }], // La feuille du prompt pour une IA (amendement du 2026-11-10, 1f). ["#/explication/12/ia", { screen: "explanation", event: "12", ai: true }], ["#/glossaire/videau/ia", { screen: "glossary", term: "videau", ai: true }], ["#/parties/mh2k3x-a1b2c3/bilan/ia", { screen: "summary", game: "mh2k3x-a1b2c3", ai: true }], [ "#/parties/mh2k3x-a1b2c3/analyse/7/ia", { screen: "analysis", game: "mh2k3x-a1b2c3", turn: 7, ai: true }, ], [ "#/parties/mh2k3x-a1b2c3/analyse/ia", { screen: "analysis", game: "mh2k3x-a1b2c3", turn: null, ai: true }, ], [ "#/position/4HPwATDgc%2FABMA/cAkAAAAAAAAA", { screen: "position", position: START, match: MATCH }, ], ["#/position/4HPwATDgc%2FABMA", { screen: "position", position: START, match: null }], ]; test("chaque adresse du tableau de l'ADR 0013 mène à son écran, et inversement", () => { for (const [hash, route] of TABLE) { assert.deepEqual(parseRoute(hash), route, hash); assert.equal(routeHash(route), hash, hash); } }); test("la feuille du prompt pour une IA se pose sur son écran, et s'en retire", () => { const route = parseRoute("#/parties/ab/analyse/3/ia"); assert.ok(isAiRoute(route)); if (isAiRoute(route)) { assert.equal(routeHash(withoutAi(route)), "#/parties/ab/analyse/3"); } assert.ok(!isAiRoute(parseRoute("#/parties/ab/analyse/3"))); // Un terme nommé « ia » reste un terme. assert.deepEqual(parseRoute("#/glossaire/ia"), { screen: "glossary", term: "ia" }); }); test("une adresse vide ou « # » est la partie en cours ; une barre finale ne change rien", () => { assert.deepEqual(parseRoute(""), { screen: "game" }); assert.deepEqual(parseRoute("#"), { screen: "game" }); assert.deepEqual(parseRoute("#/aide/"), { screen: "help", question: null }); assert.deepEqual(parseRoute("#/glossaire/blot/"), { screen: "glossary", term: "blot" }); }); test("une adresse inconnue est reconnue comme telle", () => { for (const hash of [ "#/inconnu", "#aide", "#/aide/a/b", "#/aide/Majuscules", "#/menu/plus", "#/tutoriel/0", "#/tutoriel/x", "#/tutoriel/3/b", "#/tutoriel/3/a-vous/x", "#/tutoriel/fin/a-vous", "#/lecons/0", "#/lecons/12", "#/lecons/x", "#/lecons/2/0", "#/lecons/2/x", "#/lecons/2/3/b", "#/lecons/2/3/a-vous/x", "#/lecons/2/fin/a-vous", "#/installer/x", "#/a-propos/x", "#/position", "#/position/%E0%A4%A", "#/parties/ab/autre", "#/parties/ab/bilan/2", "#/parties/ab/analyse/0", "#/parties/ab/analyse/x", "#/parties/ab/analyse/3/plus", "#/parties/AB/bilan", "#/aide/blot/ia", "#/menu/ia", "#/parties/ab/ia", "#/explication/12/ia/ia", "#/progression/x", "#/progression/ia/ia", ]) { assert.equal(parseRoute(hash).screen, "unknown", hash); } assert.equal(routeHash(parseRoute("#/inconnu")), "#/inconnu"); }); test("un lien de position collé sans encodage (+ et / du base64) est compris", () => { assert.deepEqual(parseRoute(`#/position/${START}/${MATCH}`), { screen: "position", position: START, match: MATCH, }); assert.deepEqual(parseRoute("#/position/jGfkASjg8wcBMA/cAkAAAAAAAAA"), { screen: "position", position: "jGfkASjg8wcBMA", match: MATCH, }); // + reste un + (pas une espace), encodé ou non. const plus = "AAAA+AAAAAAAAA"; assert.equal( routeHash({ screen: "position", position: plus, match: null }), "#/position/AAAA%2BAAAAAAAAA", ); assert.deepEqual(parseRoute("#/position/AAAA%2BAAAAAAAAA"), { screen: "position", position: plus, match: null, }); assert.deepEqual(parseRoute(`#/position/${plus}`), { screen: "position", position: plus, match: null, }); }); test("un identifiant de position de mauvaise longueur arrive tel quel au moteur, qui jugera", () => { assert.deepEqual(parseRoute("#/position/abc"), { screen: "position", position: "abc", match: null, }); }); test("l'identifiant d'une question : le titre sans accents ni ponctuation", () => { assert.equal(slug("Jouer un coup, puis l'annuler"), "jouer-un-coup-puis-l-annuler"); assert.equal(slug(" Été : déjà vu ? "), "ete-deja-vu"); assert.equal( slug("Rejouer d'ici sans perdre l'original"), "rejouer-d-ici-sans-perdre-l-original", ); }); test("les écrans de l'aide : jouer, les mots du jeu, à propos", () => { assert.ok(isHelpScreen(parseRoute("#/aide/x"))); assert.ok(isHelpScreen(parseRoute("#/glossaire"))); assert.ok(isHelpScreen(parseRoute("#/a-propos"))); assert.ok(!isHelpScreen(parseRoute("#/menu"))); }); test("quitter un écran revient à la dernière adresse qui n'en est pas", () => { const help = (route: Route) => isHelpScreen(route); // Partie, aide, une question : « Retour » remonte de deux pas. assert.equal(stepsBack(["#/", "#/aide", "#/aide/q"], help), -2); // Le menu ouvert depuis l'aide : on revient à l'aide. const menu = (route: Route) => route.screen === "menu"; assert.equal(stepsBack(["#/", "#/aide", "#/menu"], menu), -1); // Ouvert directement dans un nouvel onglet : rien avant, on remplace. assert.equal(stepsBack(["#/aide/q"], help), null); assert.equal(stepsBack(["#/aide", "#/glossaire/blot"], help), null); });