From cb42052c8014a4d3c9f46cd6884bc69001820e3e Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Thu, 12 Nov 2026 02:47:38 +0100 Subject: [PATCH] Test navigateur : attendre l'enregistrement de la nouvelle partie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sous Firefox, sur disque lent, l'enregistrement de la nouvelle partie attend dans la file derrière l'analyse de la partie finie ; le test lisait IndexedDB trop tôt. Il attend désormais que la partie en cours change. Test unitaire : rien n'est perdu avec un stockage lent. Co-Authored-By: Claude Opus 5.5 --- app/tests/storage.test.ts | 51 +++++++++++++++++++++++++++++++++++++++ tests/browser/play.mjs | 50 +++++++++++++++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/app/tests/storage.test.ts b/app/tests/storage.test.ts index a6bc7ec..1f4ed8d 100644 --- a/app/tests/storage.test.ts +++ b/app/tests/storage.test.ts @@ -222,6 +222,57 @@ test("les identifiants de partie sont distincts", () => { assert.match(newGameId(), /^[0-9a-z]+-[0-9a-z]{6}$/); }); +// Régression du test navigateur « mes parties » sous Firefox (disque lent) : +// la nouvelle partie n'est pas perdue quand son enregistrement attend +// derrière ceux, lents, de l'analyse de la partie finie ; seule une lecture +// directe du stockage, hors de la file, peut la précéder. +test("stockage lent : la nouvelle partie est enregistrée après l'analyse de l'ancienne", async () => { + const storage = memoryStorage(); + const raw = createKeyValueBackend(storage); + const pause = () => new Promise((resolve) => setTimeout(resolve, 30)); + const slow = { + ...raw, + write: async (...args: Parameters) => { + await pause(); + await raw.write(...args); + }, + setCurrentId: async (id: string) => { + await pause(); + await raw.setCurrentId(id); + }, + }; + const games = createGameLibrary(slow, { now: clock() }); + const engine = {} as Engine; + const session = createGameSession(games, { + start: () => Game.start(engine, { dice: fixedDice([5, 2]) }), + resume: (j) => Game.resume(engine, j), + }); + const finished = session.newGame(); + const finishedId = session.shownId as string; + session.save(finished.journal); + assert.equal((await games.current())?.summary.id, finishedId); + + // L'analyse de la partie finie s'enregistre en arrière-plan, puis + // « Rejouer » : nouvelle partie, enregistrée sans attendre. + for (let i = 0; i < 5; i++) { + void games.saveAnalyses(finishedId, finished.journal, { level: "2ply" }); + } + const next = session.newGame(); + session.save(next.journal); + const nextId = session.shownId as string; + assert.notEqual(nextId, finishedId); + + // Lue directement, tout de suite : encore l'ancienne partie. + assert.equal(await raw.currentId(), finishedId); + // Lue par la bibliothèque, après les écritures en attente : la nouvelle, + // avec son journal ; l'ancienne reste là. + const current = await games.current(); + assert.equal(current?.summary.id, nextId); + assert.deepEqual(current?.journal, next.journal); + assert.equal(await raw.currentId(), nextId); + assert.deepEqual((await games.load(finishedId))?.journal, finished.journal); +}); + test("session : « Reprendre l'ancienne » par l'identifiant, et reprise d'une branche", async () => { const games = library(); // Game.start et Game.resume n'interrogent pas le moteur. diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 2b47443..be3b5dd 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -164,6 +164,45 @@ async function readJournal() { ); } +/** L'identifiant de la partie en cours enregistrée dans IndexedDB + * (magasin settings, clé « current »), ou null. */ +async function savedGameId() { + return page.evaluate( + () => + new Promise((resolve, reject) => { + const open = indexedDB.open("ustagammon"); + open.onerror = () => reject(open.error); + open.onsuccess = () => { + const db = open.result; + const current = db.transaction("settings").objectStore("settings").get("current"); + current.onsuccess = () => { + db.close(); + resolve(current.result?.value ?? null); + }; + }; + }), + ); +} + +/** + * Attend que la partie en cours enregistrée ne soit plus oldId, et renvoie + * le temps d'attente en ms (null au bout de 30 s). L'application enregistre + * sans attendre l'écriture, dans une file (app/src/storage/game-library.ts) : + * sous Firefox, sur un disque lent et chargé, l'enregistrement de la + * nouvelle partie peut attendre derrière ceux de l'analyse de la partie + * finie, et une lecture directe d'IndexedDB trouver encore l'ancienne. + */ +async function waitForSavedGameOtherThan(oldId) { + const start = Date.now(); + while (Date.now() - start < 30_000) { + if ((await savedGameId()) !== oldId) { + return Date.now() - start; + } + await sleep(100); + } + return null; +} + /** Le nombre de parties enregistrées dans IndexedDB. */ async function savedGameCount() { await savedJournal(); // les enregistrements en cours terminés @@ -1026,6 +1065,7 @@ async function rematch() { * immédiat, bandeau 10 s. Renvoie le journal de la partie quittée. */ async function newGameFromMenu() { const before = await savedJournal(); + const beforeId = await savedGameId(); const started = () => page.evaluate(() => JSON.parse(localStorage.getItem("ustagammon.settings") ?? "{}").gamesStarted); const startedBefore = await started(); @@ -1038,6 +1078,8 @@ async function newGameFromMenu() { `bandeau : « ${await page.textContent("#banner")} »`); check(!(await page.isVisible("#menu")), "le menu reste ouvert après « Nouvelle partie »"); await waitForPlayer(); + check((await waitForSavedGameOtherThan(beforeId)) !== null, + "« Nouvelle partie » : la partie n'a pas été enregistrée (30 s)"); const after = await savedJournal(); // (Les identifiants d'événements se suivent : une partie neuve peut avoir // la même tête que la précédente ; on compte les parties commencées.) @@ -1617,11 +1659,17 @@ step("mes parties"); // recharger la page. { const finished = await savedJournal(); + const finishedId = await savedGameId(); await rematch(); await waitForPlayer(); + // L'état observable, et non un délai : la nouvelle partie devient la + // partie en cours enregistrée. + const waited = await waitForSavedGameOtherThan(finishedId); + check(waited !== null, "la nouvelle partie n'a pas été enregistrée (30 s)"); + log(`nouvelle partie enregistrée ${waited} ms après la main au joueur`); const second = await savedJournal(); check(second.head !== finished.head || second.events.length !== finished.events.length, - "la nouvelle partie n'a pas été enregistrée"); + "la nouvelle partie enregistrée a le journal de la partie finie"); await openMenu(); await page.click("#menu-games"); await page.waitForSelector("#games-veil:not([hidden]) .games-item");