Mes parties : scénarios, glossaire, test navigateur sur IndexedDB

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-03 15:36:54 +01:00
co-authored by Claude Opus 5.5
parent 274fbf6252
commit 31815134e5
12 changed files with 449 additions and 81 deletions
+85 -4
View File
@@ -12,6 +12,7 @@
// premier problème, ou si la page signale une erreur (dont une violation de
// la politique de sécurité du contenu).
import { readFileSync, writeFileSync } from "node:fs";
import { chromium, firefox } from "playwright";
const [base, browserName, shots] = process.argv.slice(2);
@@ -78,9 +79,49 @@ function check(condition, message) {
}
}
/** Les coups joués de chaque camp, d'après le journal enregistré. */
/** Le journal de la partie en cours, tel qu'enregistré dans IndexedDB
* (app/src/storage/indexeddb-backend.ts), ou null. */
async function readJournal() {
return page.evaluate(
() =>
new Promise((resolve, reject) => {
const open = indexedDB.open("ustagammon");
open.onerror = () => reject(open.error);
open.onsuccess = () => {
const db = open.result;
const tx = db.transaction(["settings", "journals"]);
const current = tx.objectStore("settings").get("current");
current.onsuccess = () => {
const id = current.result?.value;
const journal = id === undefined ? null : tx.objectStore("journals").get(id);
if (journal === null) {
db.close();
resolve(null);
return;
}
journal.onsuccess = () => {
db.close();
resolve(journal.result?.journal ?? null);
};
};
};
}),
);
}
/** Le journal enregistré, une fois les enregistrements en cours terminés
* (l'application enregistre sans attendre) : deux lectures identiques. */
async function savedJournal() {
return page.evaluate(() => JSON.parse(localStorage.getItem("ustagammon.current-game")));
let last = await readJournal();
for (let i = 0; i < 50; i++) {
await new Promise((resolve) => setTimeout(resolve, 100));
const next = await readJournal();
if (JSON.stringify(next) === JSON.stringify(last)) {
return next;
}
last = next;
}
throw new Error("le journal enregistré ne se stabilise pas");
}
function countMoves(journal, side) {
@@ -123,8 +164,7 @@ async function waitForPlayer() {
return (
`${busy} ; « ${text("info")} » ; « ${text("prompt")} » ; moteur : ` +
`${engine.sent} commandes, ${engine.received} réponses, dernière ` +
`${JSON.stringify(engine.last)} ; journal ` +
localStorage.getItem("ustagammon.current-game")
`${JSON.stringify(engine.last)}`
);
})
.catch((error) => `page illisible (${error.message})`);
@@ -487,6 +527,47 @@ if (computerMs.length > 0) {
}
await shot("4-fin-de-partie");
step("mes parties");
// Mes parties (IndexedDB) : une deuxième partie commencée, les deux
// listées, la première exportée en SGF puis reprise, après rechargement.
{
const finished = await savedJournal();
await page.click("#again");
await waitForPlayer();
const second = await savedJournal();
check(second.head !== finished.head, "la nouvelle partie n'a pas été enregistrée");
await page.click("#games-button");
await page.waitForSelector("#games-dialog[open] .games-item");
const items = page.locator("#games-dialog .games-item");
check((await items.count()) === 2, `${await items.count()} parties listées au lieu de 2`);
const oldest = items.nth(1);
await oldest.locator('[data-action="branches"]').click();
await oldest.locator(".games-branch").first().waitFor();
const branchCount = await oldest.locator(".games-branch").count();
const [download] = await Promise.all([
page.waitForEvent("download"),
oldest.locator('[data-action="export"]').click(),
]);
const sgf = readFileSync(await download.path(), "utf8");
writeFileSync(`${shots}/${browserName}-partie.sgf`, sgf);
check(sgf.startsWith("(;FF[4]GM[6]"), `export SGF inattendu : ${sgf.slice(0, 40)}`);
check(
(sgf.match(/;[BW]\[\d\d/g) ?? []).length === countMoves(finished, "player") + countMoves(finished, "computer"),
"export SGF : nombre de coups différent du journal",
);
await shot("4b-mes-parties");
await collectViolations();
await Promise.all([page.waitForEvent("load"), oldest.locator('[data-action="resume"]').click()]);
await waitForPlayer();
const resumed = await savedJournal();
check(resumed.head === finished.head, "la première partie n'a pas été reprise");
check((await page.textContent("#prompt")) === result, "écran différent après la reprise");
log(
`mes parties : 2 parties listées (${branchCount} branche(s) pour la première), ` +
`SGF exporté (${sgf.length} octets), première partie reprise après rechargement`,
);
}
step("aide");
// Aide : scénario et glossaire lus dans les copies de docs/.
await page.click("#help-button");