From a01f605d555499a14122506a81028857dc08f821 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 17 Sep 2026 22:33:05 +0200 Subject: [PATCH] Cover task planning and recurring reset workflows --- .beads/interactions.jsonl | 1 + docs/FEATURE_CHECKLIST.md | 1 + e2e/task-planner.spec.js | 108 ++++++++++++++++++++++++++++++++ tests/static-toolboxes.test.mjs | 15 ----- 4 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 e2e/task-planner.spec.js diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index df07ce1..90eec4c 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -53,3 +53,4 @@ {"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"}} {"id":"int-bb9e0e4a748e0fa71f7a88e8d5f1b9c1","kind":"field_change","created_at":"2026-09-17T15:41:40.939310492Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} {"id":"int-8ce072a6196bda5d63c77776fe0a1c14","kind":"field_change","created_at":"2026-09-17T16:06:03.481316789Z","actor":"Shinuwa","issue_id":"sokko-g-zsd","extra":{"field":"status","new_value":"open","old_value":"in_progress"}} +{"id":"int-d8c4d7fe12bbb2589779d4fe3d920ba7","kind":"field_change","created_at":"2026-09-17T20:29:55.005289442Z","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 96657d1..0bbeb81 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -31,6 +31,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - 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. - Les parcours `e2e/table.spec.js` vérifient les en-têtes éditables sans changer les références des formules, le redimensionnement sans restauration des cellules supprimées, la navigation et la sélection clavier, la copie des valeurs TSV, le collage et l'effacement de blocs, le déplacement à la souris et l'insertion d'une plage dans une formule. Les valeurs et dimensions doivent survivre au rechargement. - Les parcours `e2e/text-exchange.spec.js` vérifient les modales partagées via Tableau et Calculateur : export désactivé à vide, validation des imports, conservation des données après erreur, brouillon effacé à la réouverture, export en lecture seule et copie exacte. L'import doit persister avec ses formules, libellés et relations ; Annuler, Fermer, Échap et le fond de modale doivent fermer correctement, avec restitution du défilement. +- Les parcours `e2e/task-planner.spec.js` couvrent création, renommage, description multiligne, catégorie, suppression et persistance. Un parent transmet sa catégorie ; un prérequis manquant avertit sans bloquer la validation. Supprimer un parent doit conserver l'enfant sans relation orpheline. Avec une horloge contrôlée, les tâches quotidiennes et hebdomadaires se réinitialisent, les ponctuelles restent cochées ; masquage et nettoyage doivent préserver les autres tâches. - 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. diff --git a/e2e/task-planner.spec.js b/e2e/task-planner.spec.js new file mode 100644 index 0000000..348b758 --- /dev/null +++ b/e2e/task-planner.spec.js @@ -0,0 +1,108 @@ +// Rôle : vérifie l'édition, les prérequis et les réinitialisations du planificateur réel. +import { expect, test } from "@playwright/test"; +import { Buffer } from "node:buffer"; + +async function setup(page) { + await page.goto("/#/toolboxes"); + await page.getByText("Importer une toolbox", { exact: true }).locator('input[type="file"]').setInputFiles({ + name: "tasks.json", mimeType: "application/json", + buffer: Buffer.from(JSON.stringify({ + toolbox: { id: "tasks", name: "Task workflows", modules: [{ id: "planner", type: "taskPlanner", title: "Tasks" }] }, + modules: { planner: { resetTime: "00:00", weeklyResetDay: 1, tasks: [ + { id: "parent", title: "Parent", type: "unique", category: "Raid" }, + { id: "child", title: "Child", type: "daily" }, + { id: "weekly", title: "Weekly", type: "weekly" }, + ] } }, + })), + }); + await page.getByRole("link", { name: "Ouvrir Task workflows", exact: true }).first().click(); + const tool = page.locator('[data-module-type="taskPlanner"]'); + await expect(tool.getByRole("checkbox", { name: "Marquer effectué Parent", exact: true })).toBeVisible(); + return tool; +} + +test("creates and edits a categorized task with a multiline description and persists deletion", async ({ page }) => { + const tool = await setup(page); + await tool.getByRole("button", { name: "Afficher l'ajout de Tasks", exact: true }).click(); + const form = tool.locator(".task-planner-add-form"); + await form.getByRole("button", { name: "Ajouter", exact: true }).click(); + await expect(tool.locator(".task-planner-title-input")).toHaveCount(3); + await form.getByPlaceholder("Nouvelle tâche", { exact: true }).fill("New task"); + await form.getByPlaceholder("Nom de catégorie", { exact: true }).fill("Crafting"); + await form.getByRole("combobox", { name: "Type", exact: true }).selectOption("unique"); + await form.getByRole("button", { name: "Ajouter", exact: true }).click(); + await expect(form.getByPlaceholder("Nouvelle tâche", { exact: true })).toHaveValue(""); + await expect(tool.getByRole("textbox", { name: "Titre", exact: true })).toHaveCount(4); + const item = tool.locator("li.task-planner-item").filter({ has: page.getByRole("button", { name: "Supprimer New task", exact: true }) }); + await item.getByRole("textbox", { name: "Titre", exact: true }).fill("Renamed"); + await item.getByRole("textbox", { name: "Titre", exact: true }).press("Enter"); + await tool.getByRole("button", { name: "Afficher la description Renamed", exact: true }).click(); + const description = tool.getByRole("textbox", { name: "Description Renamed", exact: true }); + await description.fill("First line"); + await description.press("End"); + await description.press("Enter"); + await description.pressSequentially("Second line"); + await tool.getByRole("textbox", { name: "Catégorie Renamed", exact: true }).click(); + await page.reload(); + await tool.getByRole("button", { name: "Afficher la description Renamed", exact: true }).click(); + await expect(description).toHaveValue("First line\nSecond line"); + await expect(tool.getByRole("textbox", { name: "Catégorie Renamed", exact: true })).toHaveValue("Crafting"); + await tool.getByRole("button", { name: "Supprimer Renamed", exact: true }).click(); + await page.reload(); + await expect(tool.locator(".task-planner-title-input")).toHaveCount(3); + await expect(tool.getByRole("button", { name: "Supprimer Renamed", exact: true })).toHaveCount(0); +}); + +test("parent links inherit categories, warn without blocking completion and are removed when the parent is deleted", async ({ page }) => { + const tool = await setup(page); + await tool.getByRole("button", { name: "Réglages de la tâche Child", exact: true }).click(); + await tool.getByRole("combobox", { name: "Définir un parent", exact: true }).selectOption({ label: "Parent" }); + await tool.getByRole("checkbox", { name: "Pré requis Parent", exact: true }).check(); + await tool.getByRole("button", { name: "Afficher la description Child", exact: true }).click(); + const category = tool.getByRole("textbox", { name: "Catégorie Child", exact: true }); + await expect(category).toBeDisabled(); + await expect(category).toHaveValue("Raid"); + await tool.getByRole("checkbox", { name: "Marquer effectué Child", exact: true }).check(); + await expect(tool.getByText("Pré requis parent non effectué", { exact: true })).toBeVisible(); + await expect(tool.getByRole("checkbox", { name: "Marquer non effectué Child", exact: true })).toBeChecked(); + await tool.getByRole("checkbox", { name: "Marquer effectué Parent", exact: true }).check(); + await expect(tool.locator(".task-planner-warning")).toHaveCount(0); + await page.reload(); + await tool.getByRole("button", { name: "Réglages de la tâche Child", exact: true }).click(); + await expect(tool.getByRole("checkbox", { name: "Pré requis Parent", exact: true })).toBeChecked(); + await tool.getByRole("button", { name: "Supprimer Parent", exact: true }).click(); + await expect(tool.getByRole("checkbox", { name: "Pré requis Parent", exact: true })).toHaveCount(0); + await expect(tool.getByText("Aucun parent", { exact: true })).toBeVisible(); + await tool.getByRole("button", { name: "Afficher la description Child", exact: true }).click(); + await expect(category).toBeEnabled(); + await page.reload(); + await expect(tool.getByRole("button", { name: "Supprimer Child", exact: true })).toBeVisible(); + await expect(tool.getByRole("button", { name: "Supprimer Parent", exact: true })).toHaveCount(0); +}); + +test("recurring resets preserve unique completions and cleanup deletes only completed unique tasks", async ({ page }) => { + await page.clock.install({ time: new Date("2030-01-07T12:00:00Z") }); + const tool = await setup(page); + for (const title of ["Parent", "Child", "Weekly"]) { + await tool.getByRole("checkbox", { name: `Marquer effectué ${title}`, exact: true }).check(); + } + await page.clock.fastForward(8 * 24 * 60 * 60 * 1000); + await expect(tool.getByRole("checkbox", { name: "Marquer effectué Child", exact: true })).not.toBeChecked(); + await expect(tool.getByRole("checkbox", { name: "Marquer effectué Weekly", exact: true })).not.toBeChecked(); + await expect(tool.getByRole("checkbox", { name: "Marquer non effectué Parent", exact: true })).toBeChecked(); + const hide = tool.getByRole("button", { name: "Masquer les tâches effectuées", exact: true }); + await hide.click(); + await expect(tool.getByRole("button", { name: "Supprimer Parent", exact: true })).toHaveCount(0); + await page.reload(); + await expect(hide).toHaveAttribute("aria-pressed", "true"); + await hide.click(); + await expect(tool.getByRole("checkbox", { name: "Marquer non effectué Parent", exact: true })).toBeChecked(); + await tool.getByRole("button", { name: "Réglages du planificateur", exact: true }).click(); + const cleanup = tool.getByRole("button", { name: "Nettoyer les tâches ponctuelles effectuées", exact: true }); + await cleanup.click(); + await expect(cleanup).toBeDisabled(); + await page.reload(); + await expect(tool.locator(".task-planner-title-input")).toHaveCount(2); + await expect(tool.getByRole("checkbox", { name: "Marquer effectué Child", exact: true })).toBeVisible(); + await expect(tool.getByRole("checkbox", { name: "Marquer effectué Weekly", exact: true })).toBeVisible(); +}); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index df33d02..368da12 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -135,7 +135,6 @@ test("toolbox module registry and modules expose expected behavior", async () => const taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.jsx", "utf8"); const taskPlannerSettings = await readFile("website/src/features/toolboxes/modules/TaskPlannerSettings.jsx", "utf8"); const taskPlannerBundle = `${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`; - const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8"); const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8"); const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8"); const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8"); @@ -321,23 +320,9 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calendarUtils, /export function getCalendarOccurrences/); assert.match(calendarUtils, /CALENDAR_VIEW_MODES = \["week", "month"\]/); assert.match(calendarUtils, /export function getNextCalendarAlertTime/); - assert.match(taskPlannerModule, /export function TaskPlannerModule/); - assert.match(taskPlannerModule, /normalizeTaskPlannerData/); - assert.match(taskPlannerModule, /applyDueResets/); - assert.match(taskPlannerModule, /TaskPlannerBranch/); assert.match(taskPlannerBundle, /useGroupedReorder/); - assert.match(taskPlannerModule, /useDraftForm/); assert.match(taskPlannerBundle, /TextExchangeActions/); assert.match(taskPlannerBundle, /reorderFeatures/); - assert.match(taskPlannerBranch, /export function TaskPlannerBranch/); - assert.match(taskPlannerItem, /export function TaskPlannerItem/); - assert.match(taskPlannerBundle, /task-planner-warning/); - assert.match(taskPlannerBundle, /useInlineEdit/); - assert.match(taskPlannerBundle, /commitOnEnter: false/); - assert.match(taskPlannerBundle, /categoryLabel/); - assert.match(taskPlannerBundle, /prerequisite/); - assert.match(taskPlannerUtils, /export function getTaskParentMap/); - assert.match(taskPlannerUtils, /export function applyDueResets/); assert.doesNotMatch(taskPlannerModule, /canMoveItem:/); assert.doesNotMatch(taskPlannerModule, /canMoveGroup:/); assert.match(goalTreeModule, /export function GoalTreeModule/);