From a4268dac540d59350a0e202fb5918dc3a5cd29e7 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 14 Sep 2026 16:19:16 +0200 Subject: [PATCH] Cover calculator hierarchy and result workflows --- .beads/interactions.jsonl | 1 + docs/FEATURE_CHECKLIST.md | 1 + e2e/calculator.spec.js | 112 ++++++++++++++++++++++++++++++++ tests/static-toolboxes.test.mjs | 26 -------- 4 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 e2e/calculator.spec.js diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index a277a17..5b4b947 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -50,3 +50,4 @@ {"id":"int-e3a2bea901894d43a31eb628b588648a","kind":"field_change","created_at":"2026-09-14T07:08:33.027716428Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-20d1e0d862e6155eadf2c36eced314db","kind":"field_change","created_at":"2026-09-14T11:56:36.962825878Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-cc67f2645165e0eac9f57d71e8a34e57","kind":"field_change","created_at":"2026-09-14T12:11:10.207360007Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} +{"id":"int-f9245b926aa0f581b75f6b5904778e4e","kind":"field_change","created_at":"2026-09-14T13:08:47.57641261Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index cbfbd3d..2c860be 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -30,6 +30,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente. - Les parcours `e2e/toolbox-cover.spec.js` vérifient le choix d'icône, l'enregistrement d'une couverture 620 × 248, sa persistance après rechargement et import/export, l'annulation et les fichiers illisibles. L'association à un jeu doit afficher sa couverture sans perdre l'image personnalisée, restaurée après dissociation ; choisir une icône doit supprimer la couverture personnalisée. - Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante. +- Les parcours `e2e/calculator.spec.js` vérifient la validation des formules, l'édition parent/enfant avec recalcul et persistance, la suppression d'une branche active, le retour à un calcul racine, la réinitialisation du brouillon sans effacer les résultats, la copie pour checklist et la persistance du défilement. La réorganisation des racines doit conserver leurs descendants et refuser de changer le parent d'un enfant. - Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur. - Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu. - Vérifier le mode une colonne et deux colonnes. diff --git a/e2e/calculator.spec.js b/e2e/calculator.spec.js new file mode 100644 index 0000000..3f52129 --- /dev/null +++ b/e2e/calculator.spec.js @@ -0,0 +1,112 @@ +// Rôle : vérifie les calculs hiérarchiques et les actions du calculateur dans une toolbox réelle. +import { expect, test } from "@playwright/test"; + +async function setup(page) { + await page.goto("/#/toolboxes"); + await page.getByRole("button", { name: "Nouvelle toolbox" }).click(); + const dialog = page.getByRole("dialog", { name: "Nouvelle toolbox" }); + await dialog.getByLabel("Nom").fill("Calculator workflows"); + await dialog.getByRole("button", { name: "Créer", exact: true }).click(); + await page.getByRole("button", { name: "Ajouter un outil", exact: true }).click(); + await page.getByRole("dialog", { name: "Ajouter un outil" }).getByRole("button", { name: "Calculateur", exact: true }).click(); + return page.locator('[data-module-type="calculator"]'); +} + +function entry(tool, label) { + return tool.locator(".calculator-entry").filter({ has: tool.page().getByRole("button", { name: `Supprimer ${label}`, exact: true }) }); +} + +async function saveRoot(tool, formula, label) { + await tool.getByRole("textbox", { name: "Calcul", exact: true }).fill(formula); + await tool.getByLabel("Libellé", { exact: true }).fill(label); + await tool.getByRole("button", { name: "Enregistrer", exact: true }).click(); + await expect(entry(tool, label)).toBeVisible(); +} + +test("parent edits recalculate descendants and survive reload; deleting a parent removes its subtree", async ({ page }) => { + const tool = await setup(page); + const expression = tool.getByRole("textbox", { name: "Calcul", exact: true }); + await expression.fill("2 +"); + await expect(tool.getByRole("button", { name: "Enregistrer", exact: true })).toBeDisabled(); + await saveRoot(tool, "2 + 3 * 4", "Parent"); + await expect(entry(tool, "Parent").locator("strong")).toHaveAttribute("aria-label", "14"); + await tool.getByRole("button", { name: "Ajouter un calcul enfant Parent", exact: true }).click(); + await expect(expression).toBeFocused(); + await expect(expression).toContainText("Valeur du parent"); + await expression.press("End"); + await expression.pressSequentially(" * 3"); + await tool.getByLabel("Libellé", { exact: true }).fill("Child"); + await tool.getByRole("button", { name: "Mettre à jour", exact: true }).click(); + await expect(entry(tool, "Child").locator("strong")).toHaveAttribute("aria-label", "42"); + await entry(tool, "Parent").getByTitle("Modifier", { exact: true }).click(); + await expression.fill("20"); + await tool.getByRole("button", { name: "Mettre à jour", exact: true }).click(); + await expect(entry(tool, "Child").locator("strong")).toHaveAttribute("aria-label", "60"); + await page.reload(); + await expect(entry(tool, "Parent").locator("strong")).toHaveAttribute("aria-label", "20"); + await expect(entry(tool, "Child").locator("strong")).toHaveAttribute("aria-label", "60"); + await entry(tool, "Child").getByTitle("Modifier", { exact: true }).click(); + await tool.getByRole("button", { name: "Supprimer Parent", exact: true }).click(); + await expect(tool.locator(".calculator-entry")).toHaveCount(0); + await expect(expression).toHaveValue(""); + await expect(tool.getByRole("button", { name: "Enregistrer", exact: true })).toBeDisabled(); + await page.reload(); + await expect(tool.getByText("Aucun résultat enregistré.", { exact: true })).toBeVisible(); +}); + +test("reset clears only the draft, new calculation returns to root, and clipboard and scroll preferences persist", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + const tool = await setup(page); + await expect(tool.getByRole("button", { name: "Copier pour checklist", exact: true })).toBeDisabled(); + await saveRoot(tool, "10", "Ore"); + await tool.getByRole("button", { name: "Ajouter un calcul enfant Ore", exact: true }).click(); + await tool.getByRole("textbox", { name: "Calcul", exact: true }).fill("base * 2"); + await tool.getByLabel("Libellé", { exact: true }).fill("Bars"); + await tool.getByRole("button", { name: "Mettre à jour", exact: true }).click(); + await tool.getByRole("button", { name: "Nouveau calcul", exact: true }).click(); + await saveRoot(tool, "7", "Spare"); + await tool.getByRole("textbox", { name: "Calcul", exact: true }).fill("99"); + await tool.getByLabel("Libellé", { exact: true }).fill("Unsaved"); + await tool.getByRole("button", { name: "Réinitialiser", exact: true }).click(); + await expect(tool.getByRole("textbox", { name: "Calcul", exact: true })).toHaveValue(""); + await expect(tool.getByLabel("Libellé", { exact: true })).toHaveValue(""); + await expect(tool.locator(".calculator-entry")).toHaveCount(3); + await tool.getByRole("button", { name: "Copier pour checklist", exact: true }).click(); + await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("Ore:10\nBars:20\nSpare:7"); + const scroll = tool.getByRole("button", { name: "Liste scrollable", exact: true }); + await expect(scroll).toHaveAttribute("aria-pressed", "false"); + await scroll.click(); + await page.reload(); + await expect(scroll).toHaveAttribute("aria-pressed", "true"); + await expect(tool.locator(".calculator-tree")).toHaveCSS("overflow-y", "auto"); + await scroll.click(); + await page.reload(); + await expect(scroll).toHaveAttribute("aria-pressed", "false"); + await expect(tool.locator(".calculator-entry")).toHaveCount(3); +}); + +test("reordering roots keeps descendants attached and rejects moving a child to another parent", async ({ page }) => { + const tool = await setup(page); + await saveRoot(tool, "10", "First"); + await saveRoot(tool, "20", "Second"); + await tool.getByRole("button", { name: "Ajouter un calcul enfant First", exact: true }).click(); + await tool.getByLabel("Libellé", { exact: true }).fill("Child"); + await tool.getByRole("button", { name: "Mettre à jour", exact: true }).click(); + async function drag(label, targetLabel) { + await tool.getByRole("button", { name: `Déplacer ${label}`, exact: true }).hover(); + await page.mouse.down(); + const target = entry(tool, targetLabel); + await target.scrollIntoViewIfNeeded(); + const box = await target.boundingBox(); + await page.mouse.move(box.x + box.width / 2, box.y + box.height * 0.2, { steps: 5 }); + await page.mouse.up(); + } + await drag("Second", "First"); + await expect(tool.locator(".calculator-entry em")).toHaveText(["Second", "First", "Child"]); + await drag("Child", "Second"); + await expect(tool.locator(".calculator-entry em")).toHaveText(["Second", "First", "Child"]); + await page.reload(); + await expect(tool.locator(".calculator-entry em")).toHaveText(["Second", "First", "Child"]); + await tool.getByRole("button", { name: "Supprimer First", exact: true }).click(); + await expect(tool.locator(".calculator-entry em")).toHaveText(["Second"]); +}); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 7ab2c73..9363123 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -106,9 +106,6 @@ test("toolbox module registry and modules expose expected behavior", async () => const combosBundle = `${combosModule}\n${combosEditor}\n${combosParts}\n${combosList}\n${combosEditorState}\n${combosReorder}`; const combosUtils = await readFile("website/src/features/toolboxes/modules/combosUtils.js", "utf8"); const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8"); - const calculatorEntries = await readFile("website/src/features/toolboxes/modules/CalculatorEntries.jsx", "utf8"); - const calculatorUtils = await readFile("website/src/features/toolboxes/modules/calculatorUtils.js", "utf8"); - const calculatorBundle = `${calculatorModule}\n${calculatorEntries}\n${calculatorUtils}`; const tableModule = await readFile("website/src/features/toolboxes/modules/TableModule.jsx", "utf8"); const tableGrid = await readFile("website/src/features/toolboxes/modules/TableGrid.jsx", "utf8"); const tableGridParts = await readFile("website/src/features/toolboxes/modules/TableGridParts.jsx", "utf8"); @@ -276,12 +273,6 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(combosBundle, /setItemGroup: setComboItemCategory/); assert.doesNotMatch(combosModule, /canMoveItem:/); assert.doesNotMatch(combosModule, /canMoveGroup:/); - assert.match(calculatorModule, /export function CalculatorModule/); - assert.match(calculatorUtils, /export function calculateExpression/); - assert.match(calculatorModule, /activeParentId/); - assert.match(calculatorModule, /parentId/); - assert.match(calculatorModule, /MixTagsInput/); - assert.match(calculatorModule, /useGroupedReorder/); assert.match(moduleMetadataRegistry, /label: "Mémos"/); assert.match(moduleMetadataRegistry, /defaultTitle: "Mémos"/); assert.match(moduleMetadataRegistry, /label: "Bloc Note"/); @@ -295,25 +286,8 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(randomizerShared, /export function RandomizerHistory/); assert.match(randomizerUiUtils, /export function hasReducedMotion/); assert.match(randomizerBundle, /type="randomizer"/); - assert.match(calculatorEntries, /calculator-drag-handle/); - assert.match(calculatorModule, /scrollResults/); - assert.match(calculatorModule, /tool-split-scroll-toggle/); - assert.match(calculatorModule, /copyChecklistImport/); - assert.match(calculatorModule, /resetCalculator/); - assert.match(calculatorModule, /returnToRoot/); - assert.match(calculatorModule, /createChildEntry/); - assert.match(calculatorModule, /const childId = context\.uid\("calc"\)/); - assert.match(calculatorModule, /parentId: entry\.id/); - assert.match(calculatorModule, /formula: BASE_TOKEN/); - assert.match(calculatorModule, /pendingExpressionFocusRef/); - assert.match(calculatorModule, /setExpression\(BASE_TOKEN\)/); - assert.match(calculatorModule, /editingEntryId/); - assert.match(calculatorModule, /updateButton/); - assert.match(calculatorEntries, /onAddChild/); - assert.match(calculatorEntries, /entry\.label \|\| formatResult\(entry\.value\)/); assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /--calculator-scroll-height/); - assert.match(calculatorBundle, /CalculatorEntries/); assert.match(tableModule, /export function TableModule/); assert.match(tableBundle, /Icon name="add-row"/); assert.match(tableBundle, /Icon name="add-column"/);