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:
co-authored by
Claude Opus 5.5
parent
441bca1bd7
commit
5fdca812d9
+34
-4
@@ -2,7 +2,11 @@
|
||||
# Joue une partie complète par l'interface dans Chromium et Firefox sans
|
||||
# écran (tests/browser/play.mjs), sur le site servi par l'image de production
|
||||
# construite en local, comme tests/test_site.sh. Les captures d'écran vont
|
||||
# dans build/browser-shots/.
|
||||
# dans build/browser-shots/. Puis l'accueil, le tutoriel, l'aide,
|
||||
# l'installation et la mise à jour (tests/browser/accueil.mjs), avec leurs
|
||||
# captures dans build/browser-shots/accueil-tutoriel/.
|
||||
#
|
||||
# BROWSERS=chromium et SCRIPTS=accueil (ou play) restreignent le test.
|
||||
#
|
||||
# Outils : l'image de Playwright (navigateurs) et le paquet playwright,
|
||||
# installé une fois par npm dans build/browser-tools/ (réseau nécessaire la
|
||||
@@ -14,6 +18,8 @@ cd "$(dirname "$0")/.."
|
||||
|
||||
image="${PLAYWRIGHT_IMAGE:?lancer via make test-browser}"
|
||||
browsers="${BROWSERS:-chromium firefox}"
|
||||
# Les scripts à lancer (tests/browser/<nom>.mjs), tous par défaut.
|
||||
scripts="${SCRIPTS:-play accueil}"
|
||||
|
||||
unset MAKEFLAGS MFLAGS MAKELEVEL
|
||||
make -s package >/dev/null
|
||||
@@ -33,15 +39,39 @@ if [ ! -f "$tools/node_modules/playwright/package.json" ]; then
|
||||
"$image" npm install --no-audit --no-fund --save-exact "playwright@$version" >/dev/null
|
||||
fi
|
||||
|
||||
# Pour la mise à jour de l'application (tests/browser/accueil.mjs) : une
|
||||
# copie du site, servie par la même image mais depuis un dossier que le test
|
||||
# modifie, pour publier une deuxième version sur la même adresse.
|
||||
updates=build/browser-site
|
||||
rm -rf "$updates"
|
||||
cp -r build/site "$updates"
|
||||
update_container="$(docker run -d --rm -p 127.0.0.1::80 -v "$PWD/$updates:/srv:ro" "$site_image")"
|
||||
update_cleanup() {
|
||||
docker rm -f "$update_container" >/dev/null 2>&1 || true
|
||||
site_cleanup
|
||||
}
|
||||
trap update_cleanup EXIT
|
||||
update_port="$(docker port "$update_container" 80/tcp | head -n1 | sed 's/.*://')"
|
||||
|
||||
rm -rf build/browser-shots
|
||||
mkdir -p build/browser-shots
|
||||
cp tests/browser/play.mjs "$tools/"
|
||||
cp tests/browser/play.mjs tests/browser/accueil.mjs "$tools/"
|
||||
status=0
|
||||
for browser in $browsers; do
|
||||
# Réseau de l'hôte : le site est sur 127.0.0.1. Le service worker
|
||||
# demande un contexte sûr, ce qu'est localhost.
|
||||
if [[ " $scripts " == *" play "* ]]; then
|
||||
docker run --rm --init --ipc=host --network host --user "$(id -u):$(id -g)" -e HOME=/tmp \
|
||||
-e TRACE -v "$PWD/$tools:/tools" -v "$PWD/build/browser-shots:/shots" -w /tools "$image" \
|
||||
node play.mjs "http://localhost:$port/" "$browser" /shots || status=1
|
||||
fi
|
||||
[[ " $scripts " == *" accueil "* ]] || continue
|
||||
# Accueil, tutoriel, aide, installation et mise à jour ; captures de la
|
||||
# recette dans build/browser-shots/accueil-tutoriel/.
|
||||
docker run --rm --init --ipc=host --network host --user "$(id -u):$(id -g)" -e HOME=/tmp \
|
||||
-e TRACE -v "$PWD/$tools:/tools" -v "$PWD/build/browser-shots:/shots" -w /tools "$image" \
|
||||
node play.mjs "http://localhost:$port/" "$browser" /shots || status=1
|
||||
-v "$PWD/$tools:/tools" -v "$PWD/build/browser-shots:/shots" -v "$PWD/$updates:/site" \
|
||||
-w /tools "$image" \
|
||||
node accueil.mjs "http://localhost:$port/" "$browser" /shots \
|
||||
"http://localhost:$update_port/" /site || status=1
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
// accueil.mjs -- l'accueil, le tutoriel des règles, l'aide, l'installation
|
||||
// et la mise à jour de l'application, dans un vrai navigateur sans écran
|
||||
// (Playwright), sur le site servi par l'image de production
|
||||
// (docs/plans/2026-11-03-accueil-tutoriel-aide.md, point 7).
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
//
|
||||
// Usage : node accueil.mjs <adresse du site> <chromium|firefox> <dossier des
|
||||
// captures> <adresse du site modifiable> <dossier de ce site>
|
||||
// Lancé par tests/browser.sh. Le second site sert la même chose depuis un
|
||||
// dossier que ce script modifie : il y publie une deuxième version pour
|
||||
// essayer la mise à jour. Captures de la recette design (Chromium) dans
|
||||
// <dossier des captures>/accueil-tutoriel/, en 412×915 et 915×412.
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { chromium, firefox } from "playwright";
|
||||
|
||||
const [base, browserName, shots, updateBase, updateDir] = process.argv.slice(2);
|
||||
const browserType = { chromium, firefox }[browserName];
|
||||
if (!base || !browserType || !shots || !updateBase || !updateDir) {
|
||||
console.error("usage : node accueil.mjs <adresse> <chromium|firefox> <captures> <adresse 2> <dossier 2>");
|
||||
process.exit(2);
|
||||
}
|
||||
const full = browserName === "chromium";
|
||||
const dir = `${shots}/accueil-tutoriel`;
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const SIZES = [
|
||||
[412, 915],
|
||||
[915, 412],
|
||||
];
|
||||
|
||||
const browser = await browserType.launch();
|
||||
const problems = [];
|
||||
const log = (text) => console.log(`[${browserName}] ${text}`);
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
let captures = 0;
|
||||
|
||||
function check(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Un contexte neuf (stockage vide), avec des réglages déjà enregistrés si
|
||||
* settings est donné. Erreurs de la page et violations de la CSP relevées. */
|
||||
async function newPage(settings = null, url = base) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 412, height: 915 },
|
||||
deviceScaleFactor: 1,
|
||||
locale: "fr-FR",
|
||||
});
|
||||
await context.addInitScript((saved) => {
|
||||
window.__csp = [];
|
||||
document.addEventListener("securitypolicyviolation", (e) =>
|
||||
window.__csp.push(`${e.effectiveDirective} ${e.blockedURI}`),
|
||||
);
|
||||
// Un élément est-il à l'écran ? (Utilisé par les attentes.)
|
||||
window.__shown = (id) => {
|
||||
const el = document.getElementById(id);
|
||||
return el !== null && !el.closest("[hidden]") && el.getClientRects().length > 0;
|
||||
};
|
||||
window.__vibrations = [];
|
||||
Object.defineProperty(Navigator.prototype, "vibrate", {
|
||||
configurable: true,
|
||||
value: (pattern) => {
|
||||
window.__vibrations.push(String(pattern));
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (saved !== null && localStorage.getItem("ustagammon.settings") === null) {
|
||||
localStorage.setItem("ustagammon.settings", saved);
|
||||
}
|
||||
}, settings === null ? null : JSON.stringify(settings));
|
||||
const page = await context.newPage();
|
||||
page.on("pageerror", (error) => problems.push(`erreur de la page : ${error.message}`));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
problems.push(`console : ${message.text()}`);
|
||||
}
|
||||
});
|
||||
page.on("dialog", (dialog) => {
|
||||
problems.push(`boîte native : ${dialog.type()}`);
|
||||
void dialog.dismiss();
|
||||
});
|
||||
await page.goto(url);
|
||||
return { context, page };
|
||||
}
|
||||
|
||||
/** Attend que test (évalué dans la page) soit vrai, au plus ms. (Pas de
|
||||
* waitForFunction : Playwright y passe par eval, que la CSP interdit.) */
|
||||
async function until(page, what, test, arg, ms = 30_000) {
|
||||
let last = null;
|
||||
for (const end = Date.now() + ms; Date.now() < end; ) {
|
||||
last = await page.evaluate(test, arg).catch((error) => `page illisible : ${error.message}`);
|
||||
if (last === true) {
|
||||
return;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
await page.screenshot({ path: `${shots}/${browserName}-accueil-echec.png` }).catch(() => {});
|
||||
throw new Error(`${what} : attente dépassée (${JSON.stringify(last)})`);
|
||||
}
|
||||
|
||||
/** Range une capture de l'écran courant dans les deux tailles (Chromium). */
|
||||
async function capture(page, name, settle = async () => {}) {
|
||||
if (!full) {
|
||||
return;
|
||||
}
|
||||
for (const [width, height] of SIZES) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await sleep(250);
|
||||
await settle();
|
||||
await page.screenshot({ path: `${dir}/${name}-${width}x${height}.png`, fullPage: true });
|
||||
captures++;
|
||||
}
|
||||
await page.setViewportSize({ width: 412, height: 915 });
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
/** Vérifie la CSP et ferme le contexte. */
|
||||
async function close(context, page) {
|
||||
const csp = await page.evaluate(() => window.__csp).catch(() => []);
|
||||
check(csp.length === 0, `violations de la CSP : ${csp.join(", ")}`);
|
||||
await context.close();
|
||||
}
|
||||
|
||||
// ------------------------------------------------- Premier lancement
|
||||
|
||||
/** L'écran du tutoriel prêt : plateau dessiné pour cette adresse, coup
|
||||
* illustré joué (« Revoir » utilisable) s'il y en a un. */
|
||||
async function tutorialReady(page, hash) {
|
||||
await until(page, `tutoriel ${hash}`, (hash) => {
|
||||
const section = document.getElementById("tutorial");
|
||||
const replay = document.getElementById("tutorial-replay");
|
||||
return location.hash === hash && section.dataset.ready !== undefined &&
|
||||
document.querySelectorAll("#tutorial-board polygon[data-point]").length === 24 &&
|
||||
(replay.hidden || !replay.disabled);
|
||||
}, hash);
|
||||
}
|
||||
|
||||
/** Touche une case du plateau du tutoriel. */
|
||||
async function touch(page, place) {
|
||||
await page.click(`#tutorial-board [data-place="${place}"]`);
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
async function verdict(page) {
|
||||
await until(page, "verdict", () => {
|
||||
const v = document.getElementById("tutorial-feedback").dataset.verdict;
|
||||
return v === "right" || v === "wrong";
|
||||
});
|
||||
return page.evaluate(() => document.getElementById("tutorial-feedback").dataset.verdict);
|
||||
}
|
||||
|
||||
/** Les gestes des écrans « à vous » : un coup refusé, puis le bon. */
|
||||
const PRACTICE = {
|
||||
// 24/21 : ne peut plus mener à 8/5 6/5, refusé tout de suite.
|
||||
3: { wrong: [["p24", "p21"]], right: [["p8", "p5"], ["p6"]] },
|
||||
// 24/23 puis 24/18 : un coup complet qui ne frappe pas.
|
||||
4: { wrong: [["p24", "p23"], ["p24"]], right: [["p13"], ["p8"]] },
|
||||
// 6/3 : ne sort pas deux pions.
|
||||
6: { wrong: [["p6", "p3"]], right: [["p6", "off-player"], ["p3"]] },
|
||||
};
|
||||
|
||||
async function playTouches(page, touches) {
|
||||
for (const group of touches) {
|
||||
for (const place of group) {
|
||||
await touch(page, place);
|
||||
}
|
||||
await until(page, "demi-coup joué", () => !document.querySelector("#tutorial[data-busy]"));
|
||||
await sleep(150);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const { context, page } = await newPage();
|
||||
await until(page, "premier lancement", () =>
|
||||
location.hash === "#/accueil" && !document.getElementById("home-welcome").hidden);
|
||||
const first = await page.evaluate(() => ({
|
||||
topbar: getComputedStyle(document.querySelector(".topbar")).display,
|
||||
links: [...document.querySelectorAll("#home-welcome a.choice")].map((a) => a.getAttribute("href")),
|
||||
main: document.getElementById("home-main").hidden,
|
||||
}));
|
||||
check(first.topbar === "none" && first.main, `premier lancement : ${JSON.stringify(first)}`);
|
||||
check(JSON.stringify(first.links) === '["#/tutoriel/1","#/"]', `choix : ${first.links}`);
|
||||
await capture(page, "01-premier-lancement");
|
||||
log("premier lancement : « Je découvre » et « Je connais les règles », de vrais liens");
|
||||
|
||||
// Le tutoriel complet, par ses liens « Continuer ».
|
||||
await page.click("#welcome-discover");
|
||||
let screens = 0;
|
||||
for (let step = 1; step <= 8; step++) {
|
||||
const hash = `#/tutoriel/${step}`;
|
||||
await tutorialReady(page, hash);
|
||||
// La barre ne mène qu'aux étapes déjà vues.
|
||||
const links = await page.evaluate(() =>
|
||||
[...document.querySelectorAll("#tutorial-progress a")].map((a) => a.getAttribute("href")));
|
||||
const expected = Array.from({ length: step - 1 }, (_, i) => `#/tutoriel/${i + 1}`);
|
||||
check(JSON.stringify(links) === JSON.stringify(expected),
|
||||
`étape ${step} : barre de progression ${JSON.stringify(links)}`);
|
||||
const illustrated = await page.evaluate(() => !document.getElementById("tutorial-replay").hidden);
|
||||
if (illustrated) {
|
||||
check(await page.locator("#tutorial-board .trace-arrow").count() > 0,
|
||||
`étape ${step} : le coup illustré n'a pas laissé de traces`);
|
||||
// « Revoir » rejoue le coup.
|
||||
await page.click("#tutorial-replay");
|
||||
await tutorialReady(page, hash);
|
||||
}
|
||||
if (step === 2) {
|
||||
check(await page.locator("#tutorial-board .tutorial-path").count() === 1,
|
||||
"étape 2 : pas de chemin en pointillés");
|
||||
}
|
||||
await capture(page, `tutoriel-${step}`);
|
||||
screens++;
|
||||
await page.click("#tutorial-next");
|
||||
const practice = await page.evaluate(() => location.hash.endsWith("/a-vous"));
|
||||
if (!practice) {
|
||||
continue;
|
||||
}
|
||||
const avous = `${hash}/a-vous`;
|
||||
await tutorialReady(page, avous);
|
||||
check(await page.evaluate(() => document.getElementById("tutorial-next").classList.contains("secondary")),
|
||||
`étape ${step}, à vous : « Continuer » principal avant le geste`);
|
||||
await capture(page, `tutoriel-${step}-a-vous`);
|
||||
if (step === 8) {
|
||||
await page.click("#tutorial-drop");
|
||||
check(await verdict(page) === "wrong", "étape 8 : « Je passe » accepté");
|
||||
await capture(page, "tutoriel-8-a-vous-refuse");
|
||||
await page.click("#tutorial-take");
|
||||
await until(page, "étape 8 juste", () =>
|
||||
document.getElementById("tutorial-feedback").dataset.verdict === "right");
|
||||
} else {
|
||||
const moves = PRACTICE[step];
|
||||
await playTouches(page, moves.wrong);
|
||||
check(await verdict(page) === "wrong", `étape ${step} : coup faux accepté`);
|
||||
// Le pion revient : de nouveau des pions jouables.
|
||||
await until(page, "pion revenu", () => document.querySelectorAll("#tutorial-board .hit.can-move").length > 0);
|
||||
const words = await page.textContent("#tutorial-feedback");
|
||||
check(!/erreur/i.test(words), `étape ${step} : « erreur » dans le refus`);
|
||||
await capture(page, `tutoriel-${step}-a-vous-refuse`);
|
||||
await playTouches(page, moves.right);
|
||||
check(await verdict(page) === "right", `étape ${step} : bon coup refusé`);
|
||||
check(await page.locator("#tutorial-board .hit.can-move").count() === 0,
|
||||
`étape ${step} : plateau encore jouable après le bon coup`);
|
||||
}
|
||||
check(!(await page.evaluate(() => document.getElementById("tutorial-next").classList.contains("secondary"))),
|
||||
`étape ${step} : « Continuer » pas principal après le bon geste`);
|
||||
const vibrated = await page.evaluate(() => window.__vibrations.at(-1));
|
||||
check(vibrated === "12", `étape ${step} : vibration ${vibrated}`);
|
||||
await capture(page, `tutoriel-${step}-a-vous-reussi`);
|
||||
screens++;
|
||||
await page.click("#tutorial-next");
|
||||
}
|
||||
await until(page, "fin du tutoriel", () => location.hash === "#/tutoriel/fin" &&
|
||||
document.getElementById("tutorial").dataset.ready !== undefined);
|
||||
await capture(page, "tutoriel-fin");
|
||||
// Le retour du téléphone : l'écran d'avant.
|
||||
await page.goBack();
|
||||
await tutorialReady(page, "#/tutoriel/8/a-vous");
|
||||
await page.goForward();
|
||||
await until(page, "fin", () => location.hash === "#/tutoriel/fin");
|
||||
await page.click("#tutorial-next");
|
||||
await until(page, "première partie", () => location.hash === "#/" && window.__shown("game"));
|
||||
log(`tutoriel : ${screens} écrans, gestes refusés puis justes sur les 4 écrans « à vous », fin`);
|
||||
|
||||
// Le choix ne revient plus : l'accueil, ensuite.
|
||||
await page.goto(base);
|
||||
await until(page, "accueil", () => location.hash === "#/accueil" &&
|
||||
document.getElementById("home-welcome").hidden && !document.getElementById("home-main").hidden);
|
||||
await close(context, page);
|
||||
}
|
||||
|
||||
// --------------------------------------------------- Accueil, installation
|
||||
|
||||
{
|
||||
// Une partie terminée : l'invitation à installer apparaît.
|
||||
const settings = { welcomed: true, gamesStarted: 2, gamesFinished: 1, tutorialSeen: 99 };
|
||||
const { context, page } = await newPage(settings);
|
||||
await until(page, "accueil prêt", () => location.hash === "#/accueil" &&
|
||||
!document.getElementById("home-new").disabled && !document.getElementById("install-card").hidden);
|
||||
const home = await page.evaluate(() => ({
|
||||
resume: document.getElementById("home-resume").getAttribute("href"),
|
||||
primaries: [...document.querySelectorAll("#home a.button:not(.secondary), #home button:not(.secondary):not(.icon-button)")]
|
||||
.filter((el) => !el.closest("[hidden]")).length,
|
||||
rules: document.getElementById("home-rules").getAttribute("href"),
|
||||
}));
|
||||
check(home.resume === "#/" && home.primaries === 1 && home.rules === "#/tutoriel/1",
|
||||
`accueil : ${JSON.stringify(home)}`);
|
||||
await capture(page, "02-accueil");
|
||||
await capture(page, "03-carte-installation", async () => {
|
||||
await page.locator("#install-card").scrollIntoViewIfNeeded();
|
||||
});
|
||||
// Sans invite du navigateur : la marche à suivre, une feuille avec son
|
||||
// adresse, que le retour referme.
|
||||
await page.click("#install-card-button");
|
||||
await until(page, "feuille d'installation", () => location.hash === "#/installer" && window.__shown("install-sheet"));
|
||||
check((await page.locator("#install-steps li").count()) === 3, "marche à suivre sans trois étapes");
|
||||
await capture(page, "04-feuille-installation");
|
||||
await page.goBack();
|
||||
await until(page, "feuille refermée", () => location.hash === "#/accueil" && !window.__shown("install-sheet"));
|
||||
if (browserName === "chromium") {
|
||||
// L'invite de Chrome, simulée : « Installer » l'ouvre.
|
||||
await page.evaluate(() => {
|
||||
window.__prompted = 0;
|
||||
const event = new Event("beforeinstallprompt", { cancelable: true });
|
||||
event.prompt = () => {
|
||||
window.__prompted++;
|
||||
return Promise.resolve();
|
||||
};
|
||||
event.userChoice = Promise.resolve({ outcome: "dismissed" });
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
await page.click("#install-card-button");
|
||||
await until(page, "invite du navigateur", () => window.__prompted === 1);
|
||||
check(await page.evaluate(() => location.hash) === "#/accueil", "l'invite a ouvert la feuille");
|
||||
// Installée : plus aucune proposition.
|
||||
await page.evaluate(() => window.dispatchEvent(new Event("appinstalled")));
|
||||
await until(page, "installée", () => document.getElementById("install-card").hidden &&
|
||||
document.getElementById("menu-install").hidden);
|
||||
log("installation : invite de Chrome simulée, puis plus rien une fois installée");
|
||||
} else {
|
||||
await page.click("#install-later");
|
||||
await until(page, "plus tard", () => document.getElementById("install-card").hidden);
|
||||
log("installation : marche à suivre, « Plus tard »");
|
||||
}
|
||||
await close(context, page);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- Aide
|
||||
|
||||
{
|
||||
const { context, page } = await newPage({ welcomed: true, gamesStarted: 2 }, `${base}#/aide`);
|
||||
await until(page, "aide", () => window.__shown("help-play") && document.getElementById("help-content").dataset.loaded === "true");
|
||||
const help = await page.evaluate(() => ({
|
||||
steps: document.querySelectorAll("#help-play .steps, #help-play li").length,
|
||||
questions: [...document.querySelectorAll("#help-play .question h3")].map((h) => h.textContent),
|
||||
tutorial: document.getElementById("help-tutorial")?.getAttribute("href"),
|
||||
}));
|
||||
check(help.steps === 0, "aide : les étapes Gherkin sont affichées");
|
||||
check(help.questions.length >= 25 && help.tutorial === "#/tutoriel/1", `aide : ${JSON.stringify(help)}`);
|
||||
check(help.questions.filter((q) => q.endsWith("?")).length >= 20, "aide : trop peu de questions");
|
||||
await capture(page, "05-aide-jouer");
|
||||
const question = await page.evaluate(() =>
|
||||
[...document.querySelectorAll("#help-play .question")].find((q) => q.querySelector(".see-board"))?.id);
|
||||
await page.click(`#${question} h3 a`);
|
||||
await until(page, "question ouverte", (id) => document.getElementById(id).classList.contains("open"), question);
|
||||
await capture(page, "06-aide-question");
|
||||
// Le retour du téléphone referme la question.
|
||||
await page.goBack();
|
||||
await until(page, "question refermée", () => location.hash === "#/aide" && !document.querySelector(".question.open"));
|
||||
await page.click('#help-tabs a[data-screen="glossary"]');
|
||||
await until(page, "glossaire", () => window.__shown("help-glossary"));
|
||||
const before = await page.locator("#help-glossary .term-card:not([hidden])").count();
|
||||
await page.fill("#glossary-search", "sortir");
|
||||
await sleep(200);
|
||||
const after = await page.locator("#help-glossary .term-card:not([hidden])").count();
|
||||
check(after > 0 && after < before, `recherche : ${after} fiches sur ${before}`);
|
||||
await capture(page, "07-aide-glossaire-recherche");
|
||||
await page.click('#help-tabs a[data-screen="about"]');
|
||||
await until(page, "à propos", () => window.__shown("help-about"));
|
||||
const version = await page.textContent("#about-version");
|
||||
check(/^Version publiée : [0-9a-f]{12}\.$/.test(version), `version : « ${version} »`);
|
||||
await capture(page, "08-aide-a-propos");
|
||||
log(`aide : ${help.questions.length} questions, recherche dans le glossaire (${after}/${before}), ${version}`);
|
||||
await close(context, page);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- Mise à jour
|
||||
|
||||
if (browserName === "chromium") {
|
||||
const { context, page } = await newPage({ welcomed: true, gamesStarted: 2 }, `${updateBase}#/`);
|
||||
await until(page, "partie prête", () => !document.body.classList.contains("busy"), undefined, 60_000);
|
||||
// Le service worker installé et aux commandes (première installation :
|
||||
// sans attendre l'accord du joueur).
|
||||
await until(page, "service worker", async () => {
|
||||
const reg = await navigator.serviceWorker.getRegistration();
|
||||
return reg?.active?.state === "activated" && navigator.serviceWorker.controller !== null;
|
||||
}, undefined, 120_000);
|
||||
const journal = () => page.evaluate(() => localStorage.length >= 0 &&
|
||||
new Promise((resolve) => {
|
||||
const open = indexedDB.open("ustagammon");
|
||||
open.onsuccess = () => {
|
||||
const db = open.result;
|
||||
const tx = db.transaction(["settings"]);
|
||||
const current = tx.objectStore("settings").get("current");
|
||||
current.onsuccess = () => {
|
||||
db.close();
|
||||
resolve(current.result?.value ?? null);
|
||||
};
|
||||
};
|
||||
open.onerror = () => resolve(null);
|
||||
}));
|
||||
const gameBefore = await journal();
|
||||
check(await page.isHidden("#update-banner"), "bandeau de mise à jour sans nouvelle version");
|
||||
// Une deuxième version publiée : autre version.json, autre sw.js.
|
||||
writeFileSync(`${updateDir}/version.json`, '{"commit": "0123456789abcdef0123", "dirty": false}\n');
|
||||
const sw = readFileSync(`${updateDir}/sw.js`, "utf8");
|
||||
writeFileSync(`${updateDir}/sw.js`, sw.replace(/^const VERSION = "[0-9a-f]+";$/m, 'const VERSION = "0000000000000002";'));
|
||||
await page.click("#menu-button");
|
||||
await page.waitForSelector("#menu", { state: "visible" });
|
||||
await page.click("#menu-update");
|
||||
await until(page, "bandeau de mise à jour", () => window.__shown("update-banner") && window.__shown("update-apply"), undefined, 120_000);
|
||||
check((await page.textContent("#update-text")) === "Nouvelle version disponible.",
|
||||
`bandeau : « ${await page.textContent("#update-text")} »`);
|
||||
await capture(page, "09-mise-a-jour");
|
||||
await Promise.all([page.waitForEvent("load", { timeout: 60_000 }), page.click("#update-apply")]);
|
||||
await until(page, "partie reprise", () => location.hash === "#/" && !document.body.classList.contains("busy"), undefined, 60_000);
|
||||
check(await journal() === gameBefore, "après la mise à jour, ce n'est plus la même partie");
|
||||
await page.goto(`${updateBase}#/a-propos`);
|
||||
await until(page, "nouvelle version", () =>
|
||||
document.getElementById("about-version")?.textContent === "Version publiée : 0123456789ab.");
|
||||
log("mise à jour : deuxième version publiée, bandeau, « Mettre à jour », partie retrouvée, nouvelle version");
|
||||
// Rien de neuf : « Vous avez la dernière version ».
|
||||
await page.goto(`${updateBase}#/`);
|
||||
await until(page, "partie", () => !document.body.classList.contains("busy"), undefined, 60_000);
|
||||
await page.click("#menu-button");
|
||||
await page.waitForSelector("#menu", { state: "visible" });
|
||||
await page.click("#menu-update");
|
||||
await until(page, "dernière version", () =>
|
||||
document.getElementById("update-text").textContent === "Vous avez la dernière version.", undefined, 60_000);
|
||||
await close(context, page);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
if (problems.length > 0) {
|
||||
console.error(problems.join("\n"));
|
||||
process.exit(1);
|
||||
}
|
||||
log(`aucune erreur dans la page${full ? ` ; ${captures} captures dans accueil-tutoriel/` : ""}`);
|
||||
+25
-6
@@ -638,6 +638,9 @@ async function screenOf(tab) {
|
||||
term: document.querySelector("#help-glossary dt.current")?.id ?? null,
|
||||
menu: shown("menu"),
|
||||
games: shown("games"),
|
||||
home: shown("home"),
|
||||
tutorial: shown("tutorial"),
|
||||
install: shown("install-sheet"),
|
||||
listed: document.querySelectorAll("#games .games-item").length,
|
||||
expanded:
|
||||
document
|
||||
@@ -708,17 +711,19 @@ async function checkAddresses() {
|
||||
let question = null;
|
||||
let gameId = null;
|
||||
let gamesBefore = 0;
|
||||
const notYet = (s) => s.hash === "#/" && s.game && s.info.includes("n'existe pas encore");
|
||||
const addresses = [
|
||||
["#/", playing],
|
||||
["#/accueil", notYet],
|
||||
["#/accueil", (s) => s.home && !s.game && !s.tutorial],
|
||||
["#/aide", (s) => s.help && s.play && !s.glossary && !s.about && s.question === null],
|
||||
[() => `#/aide/${question}`, (s) => s.help && s.play && s.question === `aide-${question}`],
|
||||
["#/glossaire", (s) => s.help && s.glossary && !s.play && s.term === null],
|
||||
["#/glossaire/lien-de-position", (s) => s.help && s.glossary && s.term === "glossaire-lien-de-position"],
|
||||
["#/a-propos", (s) => s.help && s.about && !s.play && !s.glossary],
|
||||
["#/tutoriel", notYet],
|
||||
["#/tutoriel/2", notYet],
|
||||
["#/tutoriel", (s) => s.hash === "#/tutoriel/1" && s.tutorial && !s.game],
|
||||
["#/tutoriel/2", (s) => s.tutorial && !s.home],
|
||||
["#/tutoriel/3/a-vous", (s) => s.tutorial && s.hash === "#/tutoriel/3/a-vous"],
|
||||
["#/tutoriel/fin", (s) => s.tutorial && s.hash === "#/tutoriel/fin"],
|
||||
["#/installer", (s) => s.install && s.hash === "#/installer"],
|
||||
["#/parties", (s) => s.games && s.listed >= 2 && !s.menu],
|
||||
[() => `#/parties/${gameId}`, (s) => s.games && s.expanded === gameId],
|
||||
["#/menu", (s) => s.menu && !s.games],
|
||||
@@ -777,8 +782,20 @@ async function checkAddresses() {
|
||||
check((await tab.locator(selector).and(tab.getByRole("link")).count()) === 0,
|
||||
`${selector} : une action de jeu exposée comme un lien`);
|
||||
}
|
||||
// Le bouton « Aide » ouvre la question du moment de la partie
|
||||
// (help-context.ts) ; ici, l'onglet « Jouer » sans question.
|
||||
const helpHref = await tab.getAttribute("#help-button", "href");
|
||||
check(/^#\/aide(\/[a-z0-9-]+)?$/.test(helpHref), `bouton Aide : ${helpHref}`);
|
||||
await tab.click("#help-button");
|
||||
await expectScreen(tab, "aide par son lien", (s) => s.hash === "#/aide" && s.help && s.play);
|
||||
await expectScreen(tab, "aide par son lien", (s) => s.hash === helpHref && s.help && s.play);
|
||||
if (helpHref !== "#/aide") {
|
||||
await tab.goBack();
|
||||
await expectScreen(tab, "retour à la partie", playing);
|
||||
await tab.evaluate(() => {
|
||||
location.hash = "#/aide";
|
||||
});
|
||||
await expectScreen(tab, "aide", (s) => s.hash === "#/aide" && s.help && s.play);
|
||||
}
|
||||
for (const selector of ["#help-back", "#help-tabs a", "#help-play .question h3 a"]) {
|
||||
check(await isLink(tab, selector), `${selector} : pas des liens`);
|
||||
}
|
||||
@@ -920,7 +937,9 @@ await context.route("**/wasm/engine.data", async (route) => {
|
||||
await route.continue();
|
||||
});
|
||||
const start = Date.now();
|
||||
await page.goto(base);
|
||||
// « #/ » : la partie ; sans adresse d'écran, l'application s'ouvrirait sur
|
||||
// l'accueil (tests/browser/accueil.mjs).
|
||||
await page.goto(`${base}#/`);
|
||||
await page.waitForSelector("#board polygon[data-point]");
|
||||
{
|
||||
const loading = await page.evaluate(() => ({
|
||||
|
||||
Reference in New Issue
Block a user