Add text import export for toolbox modules
This commit is contained in:
parent
a9b5ca423a
commit
117b7b79e1
27 changed files with 1430 additions and 181 deletions
|
|
@ -2,11 +2,29 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
|
||||
import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeChecklistData, normalizeCombosData, normalizeEquipmentPlannerData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, 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() {
|
||||
let index = 0;
|
||||
return {
|
||||
uid: (prefix) => `${prefix}-${index += 1}`,
|
||||
normalizeChecklistData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCombosData,
|
||||
normalizeCalculatorData,
|
||||
normalizeTableData,
|
||||
normalizeTimerData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeUrl
|
||||
};
|
||||
}
|
||||
|
||||
test("colon text import keeps urls intact after the first separator", () => {
|
||||
assert.deepEqual(parseColonImportLines("Build:https://example.com/build?class=rogue\nPotion:10"), [
|
||||
{ label: "Build", value: "https://example.com/build?class=rogue" },
|
||||
|
|
@ -20,6 +38,140 @@ test("colon text import accepts labels without value", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test("text exchange round trips simple toolbox modules and resets checklist progress", () => {
|
||||
const context = createTextImportContext();
|
||||
const checklist = normalizeChecklistData({
|
||||
sections: [
|
||||
{
|
||||
id: "s1",
|
||||
title: "Potions",
|
||||
items: [
|
||||
{ id: "i1", label: "Potion", qtyTarget: 10, qtyCurrent: 7 },
|
||||
{ id: "i2", label: "Mega potion", qtyTarget: 5, qtyCurrent: 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
const checklistImport = importModuleText("checklist", exportModuleText("checklist", checklist), context);
|
||||
assert.equal(checklistImport.ok, true);
|
||||
assert.deepEqual(checklistImport.data.sections[0].items.map((item) => [item.label, item.qtyTarget, item.qtyCurrent]), [
|
||||
["Potion", 10, 0],
|
||||
["Mega potion", 5, 0]
|
||||
]);
|
||||
|
||||
const links = normalizeLinksData({ links: [{ id: "l1", title: "Build", url: "https://example.com/build" }] });
|
||||
const linksImport = importModuleText("links", exportModuleText("links", links), context);
|
||||
assert.equal(linksImport.ok, true);
|
||||
assert.equal(linksImport.data.links[0].url, "https://example.com/build");
|
||||
|
||||
const counters = normalizeCountersData({ counters: [{ id: "c1", label: "Win", value: 12 }, { id: "c2", label: "Loss", value: -2 }] });
|
||||
const countersImport = importModuleText("counters", exportModuleText("counters", counters), context);
|
||||
assert.equal(countersImport.ok, true);
|
||||
assert.deepEqual(countersImport.data.counters.map((counter) => [counter.label, counter.value]), [["Win", 12], ["Loss", -2]]);
|
||||
});
|
||||
|
||||
test("text exchange imports table TSV and calculator hierarchy", () => {
|
||||
const context = createTextImportContext();
|
||||
const tableImport = importModuleText("table", "@size: 2x2\n@columns: Item\tTotal\n@rows: One\tTwo\n@tsv\nPotion\t=A1\nMega\t5", context);
|
||||
assert.equal(tableImport.ok, true);
|
||||
assert.deepEqual(normalizeTableData(tableImport.data), {
|
||||
rows: 2,
|
||||
columns: 2,
|
||||
cells: { A1: "Potion", B1: "=A1", A2: "Mega", B2: "5" },
|
||||
rowLabels: { 0: "One", 1: "Two" },
|
||||
columnLabels: { 0: "Item", 1: "Total" }
|
||||
});
|
||||
const pureTsvImport = importModuleText("table", "A\tB\n1\t2", context);
|
||||
assert.equal(pureTsvImport.ok, true);
|
||||
assert.equal(pureTsvImport.data.rows, 2);
|
||||
assert.equal(pureTsvImport.data.columns, 2);
|
||||
|
||||
const calculator = normalizeCalculatorData({
|
||||
scrollResults: true,
|
||||
entries: [
|
||||
{ id: "a", label: "Ore", value: 100 },
|
||||
{ id: "b", parentId: "a", label: "Shard", value: 500 }
|
||||
]
|
||||
});
|
||||
const calculatorImport = importModuleText("calculator", exportModuleText("calculator", calculator), context);
|
||||
assert.equal(calculatorImport.ok, true);
|
||||
assert.equal(calculatorImport.data.scrollResults, true);
|
||||
assert.equal(calculatorImport.data.entries[1].parentId, calculatorImport.data.entries[0].id);
|
||||
});
|
||||
|
||||
test("text exchange round trips combos, tasks, equipment, images and timers", () => {
|
||||
const context = createTextImportContext();
|
||||
const combos = normalizeCombosData({
|
||||
device: "xbox",
|
||||
combos: [{ id: "o1", name: "Anti air", category: "Neutral", device: "xbox", inputs: [[{ kind: "direction", value: "down" }], [{ kind: "button", value: "a", holdMs: 2000 }]] }]
|
||||
});
|
||||
const combosImport = importModuleText("combos", exportModuleText("combos", combos), context);
|
||||
assert.equal(combosImport.ok, true);
|
||||
assert.equal(combosImport.data.combos[0].inputs[1][0].holdMs, 2000);
|
||||
|
||||
const tasks = normalizeTaskPlannerData({
|
||||
resetTime: "06:00",
|
||||
tasks: [
|
||||
{ id: "a", title: "Root", type: "daily", category: "Raid", checked: true, checkedAt: 99 },
|
||||
{ id: "b", title: "Child", type: "unique", checked: true, checkedAt: 99 }
|
||||
],
|
||||
relations: [{ id: "r", fromTaskId: "b", toTaskId: "a", prerequisite: true }]
|
||||
});
|
||||
const tasksImport = importModuleText("taskPlanner", exportModuleText("taskPlanner", tasks), context);
|
||||
assert.equal(tasksImport.ok, true);
|
||||
assert.deepEqual(tasksImport.data.tasks.map((task) => [task.title, task.checked, task.checkedAt]), [["Root", false, 0], ["Child", false, 0]]);
|
||||
assert.equal(tasksImport.data.relations[0].prerequisite, true);
|
||||
|
||||
const equipment = normalizeEquipmentPlannerData({
|
||||
types: [{ id: "t", title: "Weapons", icon: "sword" }],
|
||||
equipments: [{
|
||||
id: "e",
|
||||
typeId: "t",
|
||||
name: "Blade",
|
||||
icon: "sword",
|
||||
characteristics: [{ id: "v", category: "Stats", icon: "sword", name: "Attack", value: 12 }],
|
||||
socketItems: [
|
||||
{ id: "j", name: "Jewel", shape: "jewel", color: "yellow", bonuses: [{ id: "b", category: "Skill", name: "Focus", value: 1 }] },
|
||||
{ id: "k", name: "Materia", shape: "ball", color: "blue", bonuses: [] }
|
||||
],
|
||||
socketLinks: [{ id: "u", fromSocketItemId: "j", toSocketItemId: "k" }],
|
||||
materials: [{ id: "m", name: "Ore", qty: 4 }]
|
||||
}]
|
||||
});
|
||||
const equipmentImport = importModuleText("equipmentPlanner", exportModuleText("equipmentPlanner", equipment), context);
|
||||
assert.equal(equipmentImport.ok, true);
|
||||
assert.equal(equipmentImport.data.equipments[0].socketItems[0].bonuses[0].name, "Focus");
|
||||
assert.equal(equipmentImport.data.equipments[0].socketLinks.length, 1);
|
||||
|
||||
const imageDataUrl = "data:image/png;base64,QUJD";
|
||||
const imagesImport = importModuleText("images", `# Map\n${imageDataUrl}`, context);
|
||||
assert.equal(imagesImport.ok, true);
|
||||
assert.equal(imagesImport.data.images[0].dataUrl, imageDataUrl);
|
||||
const annotationImport = importModuleText("imageAnnotation", `${imageDataUrl}\n@marker: 42.5,68,Entry\n@drawings: {"strokes":[]}`, context);
|
||||
assert.equal(annotationImport.ok, true);
|
||||
assert.equal(annotationImport.data.markers[0].label, "Entry");
|
||||
|
||||
const timer = normalizeTimerData({ countdowns: [{ id: "z", label: "Boss", type: "duration", durationMs: 300000, targetAt: Date.now() + 300000 }] });
|
||||
const timerImport = importModuleText("timer", exportModuleText("timer", timer), context);
|
||||
assert.equal(timerImport.ok, true);
|
||||
assert.equal(timerImport.data.countdowns[0].durationMs, 300000);
|
||||
assert.ok(timerImport.data.countdowns[0].targetAt > Date.now());
|
||||
});
|
||||
|
||||
test("text exchange rejects invalid imports without data", () => {
|
||||
const context = createTextImportContext();
|
||||
const invalidLink = importModuleText("links", "Not a url", context);
|
||||
const invalidImage = importModuleText("images", "data:text/plain;base64,QUJD", context);
|
||||
const invalidMarker = importModuleText("imageAnnotation", "data:image/png;base64,QUJD\n@marker: 120,0,Bad", context);
|
||||
assert.equal(invalidLink.ok, false);
|
||||
assert.match(invalidLink.error, /^Ligne 1:/);
|
||||
assert.equal(invalidImage.ok, false);
|
||||
assert.match(invalidImage.error, /^Ligne 1:/);
|
||||
assert.equal(invalidMarker.ok, false);
|
||||
assert.match(invalidMarker.error, /^Ligne 2:/);
|
||||
assert.equal(importModuleText("table", "\n", context).ok, false);
|
||||
});
|
||||
|
||||
test("time pattern recurrence uses configured frequency instead of next remaining delay", () => {
|
||||
const now = new Date(2026, 0, 1, 13, 23, 45, 0).getTime();
|
||||
const target = getTimePatternTargetMs("X:24:X", now);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue