Fix toolbox tutorial state across navigation
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
This commit is contained in:
parent
75d4d99a87
commit
898fd5286f
5 changed files with 111 additions and 11 deletions
|
|
@ -34,3 +34,4 @@
|
||||||
{"id":"int-80659dd55d71f2a33d1067b008bf2abd","kind":"field_change","created_at":"2026-09-11T17:30:09.794064421Z","actor":"Shinuwa","issue_id":"sokko-g-275","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed RNG draft/edit transitions and ImageViewer pre-commit resets; cover decoding now guards stale completion/errors and releases resources. Four new StrictMode E2E cases pass; npm run check:full passes with 27 E2E. No storage or style changes."}}
|
{"id":"int-80659dd55d71f2a33d1067b008bf2abd","kind":"field_change","created_at":"2026-09-11T17:30:09.794064421Z","actor":"Shinuwa","issue_id":"sokko-g-275","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed RNG draft/edit transitions and ImageViewer pre-commit resets; cover decoding now guards stale completion/errors and releases resources. Four new StrictMode E2E cases pass; npm run check:full passes with 27 E2E. No storage or style changes."}}
|
||||||
{"id":"int-6ad8b329d6c9580520514a4234fc5230","kind":"field_change","created_at":"2026-09-11T18:38:38.171744457Z","actor":"Shinuwa","issue_id":"sokko-g-rcs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed lazy game loading without synchronous effect state updates; AbortController reaches editorial, game and nested list requests. Stale outcomes ignored, navigation cache preserved. Three new StrictMode behavior tests; npm run check:full passes with 30 E2E; targeted set-state-in-effect lint passes."}}
|
{"id":"int-6ad8b329d6c9580520514a4234fc5230","kind":"field_change","created_at":"2026-09-11T18:38:38.171744457Z","actor":"Shinuwa","issue_id":"sokko-g-rcs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed lazy game loading without synchronous effect state updates; AbortController reaches editorial, game and nested list requests. Stale outcomes ignored, navigation cache preserved. Three new StrictMode behavior tests; npm run check:full passes with 30 E2E; targeted set-state-in-effect lint passes."}}
|
||||||
{"id":"int-9cb5a663c6e65593625066b25976439c","kind":"field_change","created_at":"2026-09-11T19:15:51.529868089Z","actor":"Shinuwa","issue_id":"sokko-g-5i3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Library category now derives from anchor navigation; same-page hash/history navigation works; malformed anchors handled and obsolete RAF/scroll timers cleaned up. Existing library coverage and two new behavioral E2E pass. npm run check:full passes with 32 E2E; targeted set-state-in-effect lint passes."}}
|
{"id":"int-9cb5a663c6e65593625066b25976439c","kind":"field_change","created_at":"2026-09-11T19:15:51.529868089Z","actor":"Shinuwa","issue_id":"sokko-g-5i3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Library category now derives from anchor navigation; same-page hash/history navigation works; malformed anchors handled and obsolete RAF/scroll timers cleaned up. Existing library coverage and two new behavioral E2E pass. npm run check:full passes with 32 E2E; targeted set-state-in-effect lint passes."}}
|
||||||
|
{"id":"int-4ece504edad6e21c4829485c33215835","kind":"field_change","created_at":"2026-09-12T07:06:41.579702638Z","actor":"Shinuwa","issue_id":"sokko-g-739","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Tutorial visibility now follows toolbox identity and request transitions before commit; tour keyed by toolbox resets step and start latch. Two StrictMode behavioral tests cover identity changes, one-time flag consumption, dismissal and pending requests. check:full passes with 34 E2E; targeted set-state-in-effect lint passes."}}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
||||||
- Vérifier l'affichage dans la page toolbox complète.
|
- Vérifier l'affichage dans la page toolbox complète.
|
||||||
- Vérifier l'affichage dans le panneau latéral.
|
- Vérifier l'affichage dans le panneau latéral.
|
||||||
|
- Pour le tutoriel toolbox, vérifier le démarrage unique, la fermeture et le changement de toolbox en cours de visite ; une autre toolbox doit démarrer à la première étape et ne pas consommer les indicateurs de la précédente.
|
||||||
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile.
|
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile.
|
||||||
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
||||||
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
||||||
|
|
|
||||||
39
e2e/fixtures/toolbox-tutorial-probe.js
Normal file
39
e2e/fixtures/toolbox-tutorial-probe.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Rôle : monte la page toolbox sous StrictMode pour contrôler les demandes de tutoriel et les changements d'identité.
|
||||||
|
import { createElement, StrictMode, useState } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
|
import { ToolboxPage } from "../../website/src/features/toolboxes/ToolboxPages.jsx";
|
||||||
|
|
||||||
|
export function mountToolboxTutorialProbe(element, siteContent) {
|
||||||
|
const root = createRoot(element);
|
||||||
|
const writes = [];
|
||||||
|
const pendingChanges = [];
|
||||||
|
let setInputs;
|
||||||
|
function Probe() {
|
||||||
|
const [inputs, updateInputs] = useState({
|
||||||
|
toolboxId: "a", pendingTutorialToolboxId: "",
|
||||||
|
toolboxes: ["a", "b", "c"].map((id) => ({ id, name: id, modules: [], tutorial: id !== "c" }))
|
||||||
|
});
|
||||||
|
setInputs = updateInputs;
|
||||||
|
return createElement(ToolboxPage, {
|
||||||
|
...inputs, siteContent, actions: {},
|
||||||
|
getToolboxGame: () => null,
|
||||||
|
getModuleData: (_toolbox, _module, fallback) => fallback,
|
||||||
|
updateModuleData: () => {},
|
||||||
|
updateToolbox: (toolbox) => {
|
||||||
|
writes.push(toolbox);
|
||||||
|
updateInputs((current) => ({ ...current, toolboxes: current.toolboxes.map((item) => item.id === toolbox.id ? toolbox : item) }));
|
||||||
|
},
|
||||||
|
setPendingTutorialToolboxId: (id) => {
|
||||||
|
pendingChanges.push(id);
|
||||||
|
updateInputs((current) => ({ ...current, pendingTutorialToolboxId: id }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
flushSync(() => root.render(createElement(StrictMode, null, createElement(Probe))));
|
||||||
|
return {
|
||||||
|
writes, pendingChanges,
|
||||||
|
update: (patch) => flushSync(() => setInputs((current) => ({ ...current, ...patch }))),
|
||||||
|
unmount: () => flushSync(() => root.unmount())
|
||||||
|
};
|
||||||
|
}
|
||||||
62
e2e/toolbox-tutorial.spec.js
Normal file
62
e2e/toolbox-tutorial.spec.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Rôle : vérifie les étapes, la fermeture et l'isolation des tutoriels toolbox sous StrictMode.
|
||||||
|
/* global window, document, fetch */
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
const probeUrl = `/@fs/${fileURLToPath(new URL("./fixtures/toolbox-tutorial-probe.js", import.meta.url))}`;
|
||||||
|
|
||||||
|
async function setup(page) {
|
||||||
|
await page.goto("/");
|
||||||
|
await page.evaluate(async (url) => {
|
||||||
|
const { mountToolboxTutorialProbe } = await import(url);
|
||||||
|
const siteContent = await fetch("/data/site.json").then((response) => response.json());
|
||||||
|
document.getElementById("app").style.display = "none";
|
||||||
|
const element = document.createElement("div");
|
||||||
|
document.body.prepend(element);
|
||||||
|
window.tutorialProbe = mountToolboxTutorialProbe(element, siteContent);
|
||||||
|
}, probeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("tutorial restarts on another toolbox and consumes each start flag once", async ({ page }) => {
|
||||||
|
const errors = [];
|
||||||
|
page.on("pageerror", (error) => errors.push(error.message));
|
||||||
|
await setup(page);
|
||||||
|
const tour = page.locator(".toolbox-tutorial-tooltip");
|
||||||
|
const count = tour.locator(".toolbox-tutorial-count");
|
||||||
|
await expect(count).toHaveText(/^1\//);
|
||||||
|
await tour.locator("footer button").click();
|
||||||
|
await expect(count).toHaveText(/^2\//);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({}));
|
||||||
|
await expect(count).toHaveText(/^2\//);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({ toolboxId: "b" }));
|
||||||
|
await expect(count).toHaveText(/^1\//);
|
||||||
|
expect(await page.evaluate(() => window.tutorialProbe.writes.map(({ id, tutorial }) => ({ id, tutorial })))).toEqual([
|
||||||
|
{ id: "a", tutorial: false }, { id: "b", tutorial: false }
|
||||||
|
]);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({ toolboxId: "c" }));
|
||||||
|
await expect(tour).toHaveCount(0);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({ toolboxId: "a" }));
|
||||||
|
await expect(tour).toHaveCount(0);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.unmount());
|
||||||
|
expect(errors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pending tutorial survives unrelated toolboxes and stays dismissed after rerenders", async ({ page }) => {
|
||||||
|
await setup(page);
|
||||||
|
const tour = page.locator(".toolbox-tutorial-tooltip");
|
||||||
|
await expect(tour).toBeVisible();
|
||||||
|
await tour.locator(".toolbox-tutorial-close").click();
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({ pendingTutorialToolboxId: "c" }));
|
||||||
|
await expect(tour).toHaveCount(0);
|
||||||
|
expect(await page.evaluate(() => window.tutorialProbe.pendingChanges)).toEqual([]);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({ toolboxId: "c" }));
|
||||||
|
await expect(tour.locator(".toolbox-tutorial-count")).toHaveText(/^1\//);
|
||||||
|
expect(await page.evaluate(() => window.tutorialProbe.pendingChanges)).toEqual([""]);
|
||||||
|
await tour.locator(".toolbox-tutorial-close").click();
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({}));
|
||||||
|
await expect(tour).toHaveCount(0);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.update({ pendingTutorialToolboxId: "c" }));
|
||||||
|
await expect(tour.locator(".toolbox-tutorial-count")).toHaveText(/^1\//);
|
||||||
|
await page.evaluate(() => window.tutorialProbe.unmount());
|
||||||
|
await expect(tour).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
@ -181,12 +181,17 @@ function shouldShowTutorial(toolbox, pendingTutorialToolboxId) {
|
||||||
|
|
||||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
|
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
|
||||||
const toolboxId = toolbox.id;
|
const toolboxId = toolbox.id;
|
||||||
const toolboxTutorial = toolbox.tutorial;
|
|
||||||
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||||
const moduleText = siteContent.toolboxes.modules;
|
const moduleText = siteContent.toolboxes.modules;
|
||||||
const [showTutorial, setShowTutorial] = useState(() => shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
const [showTutorial, setShowTutorial] = useState(() => shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
||||||
const currentToolboxIdRef = useRef(toolboxId);
|
const tutorialRequested = shouldShowTutorial(toolbox, pendingTutorialToolboxId);
|
||||||
|
const [previousTutorial, setPreviousTutorial] = useState({ toolboxId, requested: tutorialRequested });
|
||||||
|
// Consommer la demande de lancement ne doit pas interrompre la visite en cours.
|
||||||
|
if (previousTutorial.toolboxId !== toolboxId || previousTutorial.requested !== tutorialRequested) {
|
||||||
|
setPreviousTutorial({ toolboxId, requested: tutorialRequested });
|
||||||
|
if (previousTutorial.toolboxId !== toolboxId || tutorialRequested) setShowTutorial(tutorialRequested);
|
||||||
|
}
|
||||||
const moduleContext = {
|
const moduleContext = {
|
||||||
getModuleData,
|
getModuleData,
|
||||||
storageReady,
|
storageReady,
|
||||||
|
|
@ -222,15 +227,6 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
refresh: () => {}
|
refresh: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentToolboxIdRef.current === toolboxId) {
|
|
||||||
if (shouldShowTutorial(toolbox, pendingTutorialToolboxId)) setShowTutorial(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentToolboxIdRef.current = toolboxId;
|
|
||||||
setShowTutorial(shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
|
||||||
}, [pendingTutorialToolboxId, toolbox, toolboxId, toolboxTutorial]);
|
|
||||||
|
|
||||||
function addModule(type, initialData = null) {
|
function addModule(type, initialData = null) {
|
||||||
if (!type) return;
|
if (!type) return;
|
||||||
const module = { id: uid("mod"), type };
|
const module = { id: uid("mod"), type };
|
||||||
|
|
@ -285,6 +281,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
</section>
|
</section>
|
||||||
{!embedded && showTutorial && (
|
{!embedded && showTutorial && (
|
||||||
<GuidedTooltipTour
|
<GuidedTooltipTour
|
||||||
|
key={toolboxId}
|
||||||
steps={siteContent.toolboxes.tutorial?.items || []}
|
steps={siteContent.toolboxes.tutorial?.items || []}
|
||||||
labels={{
|
labels={{
|
||||||
dismiss: siteContent.toolboxes.tutorial?.dismissLabel,
|
dismiss: siteContent.toolboxes.tutorial?.dismissLabel,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue