Add goal tree toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-22 18:15:39 +02:00
parent e4cbe3e087
commit 59fdcb0927
63 changed files with 4062 additions and 252 deletions

View file

@ -3,8 +3,9 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
function createTextImportContext() {
@ -20,6 +21,7 @@ function createTextImportContext() {
normalizeTableData,
normalizeTimerData,
normalizeTaskPlannerData,
normalizeGoalTreeData,
normalizeEquipmentPlannerData,
normalizeImageAnnotationData,
normalizeUrl
@ -118,6 +120,22 @@ test("text exchange imports table TSV and calculator hierarchy", () => {
assert.equal(calculatorImport.data.entries[1].formula, "base*5");
});
test("goal tree text exchange imports formulas and hierarchy", () => {
const context = createTextImportContext();
const imported = importModuleText("goalTree", "- Breuvage de rage x10\n - Élixir de destruction = base * 3\n - Poudre de flamme x5\n - Sève de cèdre x7\n - Catalyseur magique x10", context);
assert.equal(imported.ok, true);
assert.equal(imported.data.nodes.length, 5);
assert.equal(imported.data.nodes[0].kind, "step");
assert.equal(imported.data.nodes[1].parentId, imported.data.nodes[0].id);
assert.equal(imported.data.nodes[1].formula, "base * 3");
assert.equal(imported.data.nodes[2].kind, "step");
assert.equal(imported.data.nodes[2].qtyCurrent, 0);
const exported = exportModuleText("goalTree", imported.data);
assert.match(exported, /Breuvage de rage x10/);
assert.match(exported, /Élixir de destruction = base \* 3/);
});
test("text exchange round trips combos, tasks, equipment, images and timers", () => {
const context = createTextImportContext();
const combos = normalizeCombosData({
@ -910,3 +928,107 @@ test("task planner storage compacts defaults and export remaps task relations",
assert.equal(exportedPlanner.relations[0].toTaskId, "a2");
assert.equal(exportedPlanner.relations[0].prerequisite, true);
});
test("goal tree data normalizes, compacts, calculates and remaps nodes", () => {
assert.equal(calculateGoalTreeFormula("base * 3", 10), 30);
assert.equal(calculateGoalTreeFormula("base / 2", 10), 5);
assert.equal(calculateGoalTreeFormula("base - 12", 10, { clamp: false }), -2);
assert.equal(calculateGoalTreeFormula("base,5", 10), null);
assert.equal(calculateGoalTreeFormula("alert(1)", 10), null);
const normalized = normalizeGoalTreeData({
orientation: "vertical",
nodes: [
{ id: "root", title: "Root", kind: "step", icon: "fire", color: "cyan", qtyTarget: 10, qtyCurrent: 1 },
{ id: "child", parentId: "root", title: "Child", formula: "base*3", qtyCurrent: 15, resetType: "daily", resetTime: "09:30", checkedAt: 99 },
{ id: "bad", parentId: "missing", title: "Bad", kind: "weird", resetType: "later" },
{ id: "list", title: "List", kind: "checklist", items: [{ id: "i1", label: "Done", qtyCurrent: 1 }, { id: "i2", label: "Todo", formula: "5", qtyCurrent: 2 }, { id: "i3", label: "" }] },
{ id: "formulaList", parentId: "root", parentIds: ["root", "list"], title: "Formula list", kind: "checklist", items: [{ id: "i4", label: "Triple", formula: "base * 3", qtyCurrent: 30 }, { id: "i5", label: "Delta", formula: "base - 12" }] }
]
});
assert.equal(normalized.orientation, "vertical");
assert.equal(normalized.nodes[2].parentId, "");
assert.equal(normalized.nodes[2].kind, "step");
assert.equal(normalized.nodes[2].resetType, "none");
assert.equal(normalized.nodes[1].resetTime, "09:30");
assert.deepEqual(normalized.nodes[4].parentIds, ["root", "list"]);
const evaluated = evaluateGoalTreeNodes(normalized.nodes);
assert.equal(evaluated[1].computedQtyTarget, 30);
assert.equal(evaluated[1].progress.ratio, 0.5);
assert.equal(evaluated[0].progress.current, 45);
assert.equal(evaluated[0].progress.target, 61);
assert.equal(evaluated[3].progress.current, 3);
assert.equal(evaluated[3].progress.target, 7);
assert.equal(evaluated[4].items[0].qtyTarget, 30);
assert.equal(evaluated[4].items[1].qtyTarget, 1);
assert.equal(evaluated[4].progress.current, 30);
const compact = compactModuleDataForStorage("goalTree", normalized);
assert.equal(compact.settings, undefined);
assert.equal(compact.orientation, "vertical");
assert.equal(compact.nodes[0].kind, undefined);
assert.equal(compact.nodes[0].icon, "fire");
assert.equal(compact.nodes[0].color, "cyan");
assert.equal(compact.nodes[0].qtyCurrent, 1);
assert.equal(compact.nodes[1].formula, "base*3");
assert.equal(compact.nodes[1].resetType, "daily");
assert.equal(compact.nodes[1].resetTime, "09:30");
assert.deepEqual(compact.nodes[3].items, [{ id: "i1", label: "Done", qtyCurrent: 1 }, { id: "i2", label: "Todo", formula: "5", qtyCurrent: 2 }, { id: "i3", label: "" }]);
assert.deepEqual(compact.nodes[4].parentIds, ["root", "list"]);
assert.deepEqual(compact.nodes[4].items, [{ id: "i4", label: "Triple", formula: "base * 3", qtyCurrent: 30 }, { id: "i5", label: "Delta", formula: "base - 12" }]);
const exported = createToolboxExportPayload(
{ id: "toolbox1", name: "Goals", modules: [{ id: "module1", type: "goalTree" }], updatedAt: "2026-01-01T00:00:00.000Z" },
{ "toolbox1:module1": compact }
);
const exportedTree = exported.modules.m1;
assert.deepEqual(exportedTree.nodes.map((node) => node.id), ["g1", "g2", "g3", "g4", "g5"]);
assert.equal(exportedTree.nodes[1].parentId, "g1");
assert.deepEqual(exportedTree.nodes[4].parentIds, ["g1", "g4"]);
assert.deepEqual(exportedTree.nodes[3].items.map((item) => item.id), ["g6", "g7", "g8"]);
assert.deepEqual(exportedTree.nodes[4].items.map((item) => item.id), ["g9", "ga"]);
});
test("goal tree parent progress uses base-scaled child bottlenecks", () => {
const normalized = normalizeGoalTreeData({
nodes: [
{ id: "root", title: "Potion de vie", kind: "step", qtyTarget: 10 },
{
id: "ingredients",
parentId: "root",
title: "Ingrédients",
kind: "checklist",
items: [
{ id: "herb", label: "Herbe magique", formula: "base", qtyCurrent: 7 },
{ id: "water", label: "Eau rajeunissante", formula: "base", qtyCurrent: 5 }
]
},
{ id: "manual", title: "Objectif manuel", kind: "step", qtyTarget: 10, qtyCurrent: 3, progressMode: "manual" },
{ id: "manualChild", parentId: "manual", title: "Enfant ignoré", kind: "step", formula: "base", qtyCurrent: 10 },
{ id: "defaultManual", title: "Validation manuelle par défaut", kind: "step", qtyTarget: 1, qtyCurrent: 1 },
{ id: "defaultManualChild", parentId: "defaultManual", title: "Validation enfant", kind: "step", qtyTarget: 1, qtyCurrent: 0 },
{ id: "forcedAuto", title: "Validation auto forcée", kind: "step", qtyTarget: 1, qtyCurrent: 1, progressMode: "auto" },
{ id: "forcedAutoChild", parentId: "forcedAuto", title: "Validation enfant auto", kind: "step", qtyTarget: 1, qtyCurrent: 0 }
]
});
const evaluated = evaluateGoalTreeNodes(normalized.nodes);
assert.equal(evaluated[0].progress.current, 5);
assert.equal(evaluated[0].progress.target, 10);
assert.equal(evaluated[0].progress.ratio, 0.5);
assert.equal(evaluated[1].progress.current, 12);
assert.equal(evaluated[1].progress.target, 20);
assert.equal(evaluated[2].progress.current, 3);
assert.equal(evaluated[2].progress.target, 10);
assert.equal(evaluated[4].progress.current, 1);
assert.equal(evaluated[4].progress.target, 1);
assert.equal(evaluated[6].progress.current, 0);
assert.equal(evaluated[6].progress.target, 1);
const compact = compactModuleDataForStorage("goalTree", normalized);
assert.equal(compact.nodes[0].progressMode, undefined);
assert.equal(compact.nodes[2].progressMode, "manual");
assert.equal(compact.nodes[4].progressMode, undefined);
assert.equal(compact.nodes[6].progressMode, "auto");
});