Add toolbox library page with editable examples
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
8eb622e05d
commit
9b72f4934c
20 changed files with 4774 additions and 31 deletions
|
|
@ -5,6 +5,7 @@ import assert from "node:assert/strict";
|
|||
import {
|
||||
validateDiablo4Affixes,
|
||||
validateEndemicData,
|
||||
validateLibraryData,
|
||||
validateMonsterData,
|
||||
validateSiteContent,
|
||||
validateTranslationKeys
|
||||
|
|
@ -15,6 +16,11 @@ test("site content json is complete", async () => {
|
|||
validateSiteContent(site);
|
||||
});
|
||||
|
||||
test("library examples are available", async () => {
|
||||
const library = JSON.parse(await readFile("website/public/data/library.json", "utf8"));
|
||||
validateLibraryData(library);
|
||||
});
|
||||
|
||||
test("mhwilds data and assets are available", async () => {
|
||||
const site = JSON.parse(await readFile("website/public/data/site.json", "utf8"));
|
||||
const games = JSON.parse(await readFile("website/public/data/games.json", "utf8"));
|
||||
|
|
|
|||
|
|
@ -32,18 +32,27 @@ export function validateSiteContent(site) {
|
|||
"navigation.home",
|
||||
"navigation.toolboxes",
|
||||
"navigation.games",
|
||||
"navigation.library",
|
||||
"navigation.about",
|
||||
"navigation.mobileLibrary",
|
||||
"navigation.mobileGames",
|
||||
"sidebar.badge",
|
||||
"sidebar.note",
|
||||
"topbar.dashboard",
|
||||
"topbar.toolbox",
|
||||
"topbar.library",
|
||||
"topbar.games",
|
||||
"gamesPage.eyebrow",
|
||||
"gamesPage.title",
|
||||
"gamesPage.description",
|
||||
"gamesPage.emptyTitle",
|
||||
"gamesPage.emptyText",
|
||||
"library.eyebrow",
|
||||
"library.title",
|
||||
"library.description",
|
||||
"library.summaryLabel",
|
||||
"library.tocLabel",
|
||||
"library.toolsLabel",
|
||||
"home.hero.eyebrow",
|
||||
"home.hero.title",
|
||||
"home.hero.description",
|
||||
|
|
@ -65,6 +74,7 @@ export function validateSiteContent(site) {
|
|||
"about.title",
|
||||
"about.description",
|
||||
"about.toolsTitle",
|
||||
"about.toolsLibraryLink",
|
||||
"about.limitsTitle",
|
||||
"about.contribute.eyebrow",
|
||||
"about.contribute.title",
|
||||
|
|
@ -76,6 +86,7 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.eyebrow",
|
||||
"toolboxes.title",
|
||||
"toolboxes.newButton",
|
||||
"toolboxes.libraryLink",
|
||||
"toolboxes.importAll",
|
||||
"toolboxes.exportAll",
|
||||
"toolboxes.importOne",
|
||||
|
|
@ -340,6 +351,16 @@ export function validateSiteContent(site) {
|
|||
|
||||
assertStringArray(site, "toolboxes.storageHelp.items", 3);
|
||||
|
||||
const librarySummaryItems = valueAt(site, "library.summaryItems");
|
||||
assert.ok(Array.isArray(librarySummaryItems), "library.summaryItems must be an array");
|
||||
assert.ok(librarySummaryItems.length >= 3, "library.summaryItems must contain at least 3 items");
|
||||
librarySummaryItems.forEach((item, index) => {
|
||||
assertNonEmptyString({ item }, `item.icon`);
|
||||
assertNonEmptyString({ item }, `item.title`);
|
||||
assertNonEmptyString({ item }, `item.text`);
|
||||
assert.equal(typeof item, "object", `library.summaryItems[${index}] must be an object`);
|
||||
});
|
||||
|
||||
const aboutLimits = valueAt(site, "about.limits");
|
||||
assert.ok(Array.isArray(aboutLimits), "about.limits must be an array");
|
||||
assert.ok(aboutLimits.length >= 3, "about.limits must contain at least 3 items");
|
||||
|
|
@ -381,6 +402,43 @@ export function validateSiteContent(site) {
|
|||
});
|
||||
}
|
||||
|
||||
export function validateLibraryData(payload) {
|
||||
assert.equal(typeof payload?.toolbox, "object", "library.toolbox must be an object");
|
||||
assert.equal(typeof payload?.modules, "object", "library.modules must be an object");
|
||||
assertNonEmptyString(payload, "toolbox.name");
|
||||
assert.ok(Array.isArray(payload.toolbox.modules), "library.toolbox.modules must be an array");
|
||||
assert.ok(payload.toolbox.modules.length >= 1, "library.toolbox.modules must not be empty");
|
||||
|
||||
const moduleIds = new Set();
|
||||
payload.toolbox.modules.forEach((module, index) => {
|
||||
assertNonEmptyString({ module }, "module.id");
|
||||
assertNonEmptyString({ module }, "module.type");
|
||||
assert.ok(!moduleIds.has(module.id), `library.toolbox.modules[${index}].id must be unique`);
|
||||
moduleIds.add(module.id);
|
||||
assert.equal(typeof payload.modules[module.id], "object", `library.modules.${module.id} must be an object`);
|
||||
});
|
||||
|
||||
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "imageAnnotation"].forEach((type) => {
|
||||
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
|
||||
});
|
||||
|
||||
const imagesModule = payload.toolbox.modules.find((module) => module.type === "images");
|
||||
const imagesData = payload.modules[imagesModule.id];
|
||||
assert.ok(Array.isArray(imagesData.images), "library images example must contain images");
|
||||
assert.ok(imagesData.images.length >= 1, "library images example must not be empty");
|
||||
imagesData.images.forEach((image, index) => {
|
||||
assertNonEmptyString({ image }, "image.id");
|
||||
assertNonEmptyString({ image }, "image.dataUrl");
|
||||
assert.match(image.dataUrl, /^data:image\//, `library images[${index}].dataUrl must be an inline image`);
|
||||
});
|
||||
|
||||
const taskModule = payload.toolbox.modules.find((module) => module.type === "taskPlanner");
|
||||
const taskData = payload.modules[taskModule.id];
|
||||
assert.ok(Array.isArray(taskData.tasks), "library task planner example must contain tasks");
|
||||
assert.ok(Array.isArray(taskData.relations), "library task planner example must contain relations");
|
||||
assert.ok(taskData.relations.length >= 1, "library task planner example must include a parent/child relation");
|
||||
}
|
||||
|
||||
function assertSnakeCase(value, path) {
|
||||
assert.equal(typeof value, "string", `${path} must be a string`);
|
||||
assert.match(value, SNAKE_CASE_RE, `${path} must be snake_case`);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ test("vite entrypoint and app shell are wired", async () => {
|
|||
const richText = await readFile("website/src/components/RichText.jsx", "utf8");
|
||||
const homePage = await readFile("website/src/pages/HomePage.jsx", "utf8");
|
||||
const aboutPage = await readFile("website/src/pages/AboutPage.jsx", "utf8");
|
||||
const libraryPage = await readFile("website/src/pages/LibraryPage.jsx", "utf8");
|
||||
const hashRouter = await readFile("website/src/router/hashRouter.js", "utf8");
|
||||
const routeContent = await readFile("website/src/router/RouteContent.jsx", "utf8");
|
||||
const bodyScrollLock = await readFile("website/src/utils/bodyScrollLock.js", "utf8");
|
||||
|
|
@ -38,8 +39,11 @@ test("vite entrypoint and app shell are wired", async () => {
|
|||
assert.match(routeContent, /export function RouteContent/);
|
||||
assert.match(routeContent, /pages\/HomePage\.jsx/);
|
||||
assert.match(routeContent, /pages\/AboutPage\.jsx/);
|
||||
assert.match(routeContent, /pages\/LibraryPage\.jsx/);
|
||||
assert.match(routeContent, /route === "\/library"/);
|
||||
assert.match(routeContent, /features\/games\/GameRoute\.jsx/);
|
||||
assert.match(shell, /export function Shell/);
|
||||
assert.doesNotMatch(shell, /content\.navigation\.library/);
|
||||
assert.match(shell, /sidebar-about-link/);
|
||||
assert.match(shell, /ImportButton/);
|
||||
assert.match(overlays, /export function AppOverlays/);
|
||||
|
|
@ -59,6 +63,9 @@ test("vite entrypoint and app shell are wired", async () => {
|
|||
assert.match(homePage, /dragon\.png/);
|
||||
assert.match(aboutPage, /export function AboutPage/);
|
||||
assert.match(aboutPage, /module-icon-\$\{tool\.icon \|\| "notepad"\}/);
|
||||
assert.match(libraryPage, /export function LibraryPage/);
|
||||
assert.match(libraryPage, /LIBRARY_TOOLBOX_ID/);
|
||||
assert.match(libraryPage, /TaskPlannerModule/);
|
||||
assert.match(bodyScrollLock, /modalLocks/);
|
||||
assert.match(bodyScrollLock, /is-modal-open/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
|||
assert.match(importButton, /accept="application\/json"/);
|
||||
assert.match(reorderHook, /export function usePointerReorder/);
|
||||
assert.match(reorderHook, /setPointerCapture/);
|
||||
assert.match(reorderHook, /elementsFromPoint/);
|
||||
assert.match(reorderHook, /elementFromPoint/);
|
||||
assert.match(groupedReorderHook, /export function useGroupedReorder/);
|
||||
assert.match(groupedReorderHook, /usePointerReorder/);
|
||||
|
|
@ -161,8 +162,11 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(combosModule, /useInlineEdit/);
|
||||
assert.match(combosModule, /useGroupedReorder/);
|
||||
assert.match(combosModule, /reorderFeatures/);
|
||||
assert.match(combosModule, /namespace: `combos-\$\{moduleId\}`/);
|
||||
assert.match(combosModule, /getGroupBoundaryProps/);
|
||||
assert.match(combosModule, /combos-category-boundary-drop-zone/);
|
||||
assert.match(combosModule, /function setComboItemCategory/);
|
||||
assert.match(combosModule, /setItemGroup: setComboItemCategory/);
|
||||
assert.doesNotMatch(combosModule, /canMoveItem:/);
|
||||
assert.doesNotMatch(combosModule, /canMoveGroup:/);
|
||||
assert.match(calculatorModule, /export function CalculatorModule/);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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 { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCombosData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeChecklistData, normalizeCombosData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
||||
|
||||
test("colon text import keeps urls intact after the first separator", () => {
|
||||
|
|
@ -60,7 +60,7 @@ test("table storage clamps dimensions and compacts non-empty cells", () => {
|
|||
rows: 200,
|
||||
columns: 40,
|
||||
cells: {
|
||||
A1: "Boss",
|
||||
A1: "Boss ",
|
||||
T50: "=A1+1",
|
||||
U51: "Ignored",
|
||||
B1: " "
|
||||
|
|
@ -79,14 +79,14 @@ test("table storage clamps dimensions and compacts non-empty cells", () => {
|
|||
|
||||
assert.equal(normalized.rows, 50);
|
||||
assert.equal(normalized.columns, 20);
|
||||
assert.deepEqual(normalized.cells, { A1: "Boss", T50: "=A1+1" });
|
||||
assert.deepEqual(normalized.cells, { A1: "Boss ", T50: "=A1+1" });
|
||||
assert.deepEqual(normalized.rowLabels, { 0: "Boss final " });
|
||||
assert.deepEqual(normalized.columnLabels, { 0: "Item final " });
|
||||
|
||||
const compact = compactModuleDataForStorage("table", normalized);
|
||||
assert.equal(compact.rows, 50);
|
||||
assert.equal(compact.columns, 20);
|
||||
assert.deepEqual(compact.cells, { A1: "Boss", T50: "=A1+1" });
|
||||
assert.deepEqual(compact.cells, { A1: "Boss ", T50: "=A1+1" });
|
||||
assert.deepEqual(compact.rowLabels, { 0: "Boss final " });
|
||||
assert.deepEqual(compact.columnLabels, { 0: "Item final " });
|
||||
assert.deepEqual(compactModuleDataForStorage("table", { rows: 10, columns: 6, cells: {}, columnLabels: { 0: "Item " } }), {
|
||||
|
|
@ -226,6 +226,26 @@ test("grouped reorder operation moves whole categories", () => {
|
|||
assert.deepEqual(moved.categoryOrder, ["Farm", "Boss"]);
|
||||
});
|
||||
|
||||
test("checklist data drops empty titled sections", () => {
|
||||
const normalized = normalizeChecklistData({
|
||||
sections: [
|
||||
{ id: "empty", title: "Empty", items: [] },
|
||||
{ id: "filled", title: "Filled", items: [{ id: "item1", label: "Potion" }] }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(normalized.sections.map((section) => section.id), ["filled"]);
|
||||
|
||||
const compact = compactModuleDataForStorage("checklist", {
|
||||
sections: [
|
||||
{ id: "empty", title: "Empty", items: [] },
|
||||
{ id: "filled", title: "Filled", items: [{ id: "item1", label: "Potion" }] }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(compact.sections.map((section) => section.id), ["filled"]);
|
||||
});
|
||||
|
||||
test("combos storage normalizes devices, steps and compact export ids", () => {
|
||||
const longText = "x".repeat(120);
|
||||
const normalized = normalizeCombosData({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue