// Tests de l'accueil et de l'invitation à installer, sans navigateur : // premier lancement, règles d'apparition de la carte d'installation, // réglages enregistrés par une version précédente. // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { homeModel } from "../src/ui/home-model.js"; import { installPlatform } from "../src/ui/install.js"; import { DEFAULT_SETTINGS, INSTALL_AGAIN_AFTER, installCardShown, 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}'); assert.equal(old.welcomed, false); assert.equal(old.gamesFinished, 0); assert.equal(old.tutorialSeen, 0); assert.equal(old.installed, false); const saved = parseSettings( '{"welcomed": true, "gamesFinished": 2, "tutorialSeen": 5, "installDismissals": 1, ' + '"installDismissedAt": 2, "installed": true}', ); assert.deepEqual(saved, { ...DEFAULT_SETTINGS, welcomed: true, gamesFinished: 2, tutorialSeen: 5, installDismissals: 1, installDismissedAt: 2, installed: true, }); assert.equal(parseSettings('{"tutorialSeen": -3, "welcomed": "oui"}').tutorialSeen, 0); }); test("carte de la partie : sans partie en cours, ni miniature ni compte de la partie finie", () => { const settings = { ...DEFAULT_SETTINGS, welcomed: true, gamesFinished: 1 }; // Partie en cours, ou moteur qui se charge : la miniature et le compte. assert.deepEqual( [homeModel(settings, true, false).board, homeModel(settings, true, false).resume], [true, true], ); assert.equal(homeModel(settings, null, false).board, true); // Partie finie : le titre et « Nouvelle partie » seulement (recette 1h). const over = homeModel(settings, false, false); assert.equal(over.board, false); assert.equal(over.resume, false); }); test("premier lancement : tant que rien n'a été fait, une seule fois", () => { assert.ok(isFirstLaunch(DEFAULT_SETTINGS)); // La partie commencée d'office au chargement ne compte pas. assert.ok(isFirstLaunch({ ...DEFAULT_SETTINGS, gamesStarted: 1 })); assert.ok(!isFirstLaunch({ ...DEFAULT_SETTINGS, welcomed: true })); assert.ok(!isFirstLaunch({ ...DEFAULT_SETTINGS, gamesStarted: 2 })); assert.ok(!isFirstLaunch({ ...DEFAULT_SETTINGS, gamesFinished: 1 })); assert.ok(!isFirstLaunch({ ...DEFAULT_SETTINGS, tutorialSeen: 1 })); }); test("invitation à installer : après la première partie terminée, une seule relance", () => { const at = (changes: Partial, installed = false) => installCardShown({ ...DEFAULT_SETTINGS, ...changes }, installed); assert.ok(!at({}), "jamais avant la première partie terminée"); assert.ok(at({ gamesFinished: 1 })); assert.ok(!at({ gamesFinished: 1 }, true), "jamais une fois installée"); assert.ok(!at({ gamesFinished: 1, installed: true })); // « Plus tard » : masquée, puis de retour après trois parties de plus. const later = { installDismissals: 1, installDismissedAt: 1 }; assert.equal(INSTALL_AGAIN_AFTER, 3); assert.ok(!at({ ...later, gamesFinished: 1 })); assert.ok(!at({ ...later, gamesFinished: 3 })); assert.ok(at({ ...later, gamesFinished: 4 })); // Une seconde fois « Plus tard » : plus jamais sur l'accueil. 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 " + "(KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; const ipad = "Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15"; const firefox = "Mozilla/5.0 (Android 14; Mobile; rv:131.0) Gecko/131.0 Firefox/131.0"; const chrome = "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/130.0 Mobile Safari/537.36"; assert.equal(installPlatform(iphone), "ios"); assert.equal(installPlatform(ipad), "ios"); assert.equal(installPlatform(firefox), "firefox"); // Sans invite du système, Chrome suit la marche à suivre de Firefox // (menu ⋮, « Installer »), la plus proche de la sienne. assert.equal(installPlatform(chrome), "firefox"); });