165 lines
6.8 KiB
JavaScript
165 lines
6.8 KiB
JavaScript
// Standalone test for chapter CRUD logic.
|
|
// Run with: node tests/run-chapters-test.mjs
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import { bundleForTest } from "./helpers/bundle.mjs";
|
|
|
|
const { mod: project, cleanupBundle: cleanupProj } = await bundleForTest({
|
|
name: "folio-project",
|
|
entryPoints: ["src/main/project.ts"],
|
|
});
|
|
const { mod: chapters, cleanupBundle: cleanupChap } = await bundleForTest({
|
|
name: "folio-chapters",
|
|
entryPoints: ["src/main/chapters.ts"],
|
|
});
|
|
const { createBook, loadBook, saveMeta, hasBook } = project;
|
|
const {
|
|
listChaptersInOrder,
|
|
getChapterContent,
|
|
createChapter,
|
|
renameChapter,
|
|
deleteChapter,
|
|
duplicateChapter,
|
|
reorderChapters,
|
|
setChapterContent,
|
|
setChapterColor,
|
|
moveInOrder,
|
|
} = chapters;
|
|
|
|
let failures = 0;
|
|
function ok(cond, msg) {
|
|
if (cond) console.log(" ok -", msg);
|
|
else { failures++; console.error("FAIL -", msg); }
|
|
}
|
|
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "folio-ch-"));
|
|
createBook(dir, "Test Book");
|
|
|
|
// create
|
|
const c1 = createChapter(dir, "Introduction");
|
|
ok(c1.id.startsWith("introduction-"), "create returns id slug: " + c1.id);
|
|
ok(fs.existsSync(path.join(dir, c1.file)) && fs.existsSync(path.join(dir, "chapters")), "chapter file + chapters dir created");
|
|
const c2 = createChapter(dir, "Money");
|
|
const c3 = createChapter(dir, "Conclusion");
|
|
const meta = loadBook(dir);
|
|
ok(meta.chapterOrder.length === 3, "chapterOrder has 3 chapters");
|
|
ok(listChaptersInOrder(meta).length === 3, "listChaptersInOrder returns 3");
|
|
|
|
// content default empty
|
|
ok(getChapterContent(dir, c1.id) === "", "new chapter content empty");
|
|
|
|
// write content + read back
|
|
fs.writeFileSync(path.join(dir, c1.file), "Hello world this is text", "utf-8");
|
|
ok(getChapterContent(dir, c1.id) === "Hello world this is text", "chapter content read back");
|
|
|
|
// getChapterContent throws on unknown id
|
|
let threw = false;
|
|
try { getChapterContent(dir, "nope"); } catch { threw = true; }
|
|
ok(threw, "getChapterContent throws on unknown id");
|
|
|
|
// rename
|
|
renameChapter(dir, c2.id, "Economics");
|
|
ok(loadBook(dir).chapters[c2.id].title === "Economics", "rename updates title");
|
|
|
|
// duplicate
|
|
const dup = duplicateChapter(dir, c1.id);
|
|
ok(dup.title === "Introduction (copy)", "duplicate default title");
|
|
ok(dup.id !== c1.id, "duplicate has new id");
|
|
ok(getChapterContent(dir, dup.id) === "Hello world this is text", "duplicate copies content");
|
|
ok(loadBook(dir).chapterOrder.slice(-1)[0] === dup.id, "duplicate appended to order");
|
|
|
|
// setChapterContent writes markdown to disk by chapter id
|
|
setChapterContent(dir, c1.id, "# Introduction\n\nWelcome.");
|
|
ok(getChapterContent(dir, c1.id) === "# Introduction\n\nWelcome.", "setChapterContent persists markdown");
|
|
let threw2 = false;
|
|
try { setChapterContent(dir, "nope", "x"); } catch { threw2 = true; }
|
|
ok(threw2, "setChapterContent throws on unknown id");
|
|
|
|
// delete
|
|
const c3File = path.join(dir, c3.file);
|
|
deleteChapter(dir, c3.id);
|
|
const mAfter = loadBook(dir);
|
|
ok(!mAfter.chapters[c3.id], "chapter removed from map");
|
|
ok(!mAfter.chapterOrder.includes(c3.id), "chapter removed from order");
|
|
ok(!fs.existsSync(c3File), "deleted chapter file removed from disk");
|
|
|
|
// reorder
|
|
const order = loadBook(dir).chapterOrder;
|
|
reorderChapters(dir, [order[1], order[0]]);
|
|
ok(loadBook(dir).chapterOrder[0] === order[1] && loadBook(dir).chapterOrder[1] === order[0], "reorder persists");
|
|
|
|
// reorder rejects invalid id
|
|
threw = false;
|
|
try { reorderChapters(dir, ["bogus", order[0]]); } catch { threw = true; }
|
|
ok(threw, "reorder throws on invalid id");
|
|
|
|
// ---- chapter color ----
|
|
const col = createChapter(dir, "Colorful");
|
|
setChapterColor(dir, col.id, "#22c55e");
|
|
ok(loadBook(dir).chapters[col.id].color === "#22c55e", "setChapterColor assigns color");
|
|
setChapterColor(dir, col.id, "#3b82f6");
|
|
ok(loadBook(dir).chapters[col.id].color === "#3b82f6", "setChapterColor changes color");
|
|
setChapterColor(dir, col.id, null);
|
|
ok(!("color" in loadBook(dir).chapters[col.id]), "setChapterColor removes color (null)");
|
|
|
|
// color travels with the chapter on reorder
|
|
setChapterColor(dir, col.id, "#e11d48");
|
|
const before = loadBook(dir).chapterOrder.slice();
|
|
reorderChapters(dir, [before[before.length - 1], ...before.slice(0, -1)]);
|
|
ok(loadBook(dir).chapters[col.id].color === "#e11d48", "color persists after reorder");
|
|
|
|
// duplicate does not inherit the source color
|
|
const colDup = duplicateChapter(dir, col.id);
|
|
ok(!("color" in loadBook(dir).chapters[colDup.id]), "duplicate starts without color");
|
|
|
|
// setChapterColor throws on unknown id
|
|
threw = false;
|
|
try { setChapterColor(dir, "nope", "#fff"); } catch { threw = true; }
|
|
ok(threw, "setChapterColor throws on unknown id");
|
|
|
|
// empty title throws
|
|
threw = false;
|
|
try { createChapter(dir, " "); } catch { threw = true; }
|
|
ok(threw, "createChapter rejects empty title");
|
|
|
|
// moveInOrder (pure reorder math used by drag & keyboard reorder)
|
|
ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 0, 2)) === JSON.stringify(["b", "c", "a", "d"]), "move item to middle");
|
|
ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 3, 0)) === JSON.stringify(["d", "a", "b", "c"]), "move last to top");
|
|
ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 1, 1)) === JSON.stringify(["a", "b", "c", "d"]), "no-op on same index");
|
|
|
|
// ---- export (combineMarkdown + buildZip) ----
|
|
const { mod: exportMod, cleanupBundle: cleanupExport } = await bundleForTest({
|
|
name: "folio-export",
|
|
entryPoints: ["src/main/export.ts"],
|
|
format: "cjs",
|
|
});
|
|
const { combineMarkdown, buildZip } = exportMod.default;
|
|
|
|
const b1 = createChapter(dir, "Intro");
|
|
setChapterContent(dir, b1.id, "# Intro\n\nHello world\n\n- a\n- b");
|
|
const b2 = createChapter(dir, "Scene");
|
|
setChapterContent(dir, b2.id, "## Scene one\n\nIt was a night.");
|
|
const combined = combineMarkdown(dir);
|
|
ok(combined.includes("# Test Book"), "combined markdown starts with book title");
|
|
ok(combined.includes("## Intro"), "combined markdown has chapter heading (Intro)");
|
|
ok(combined.includes("## Scene"), "combined markdown has chapter heading (Scene)");
|
|
ok(combined.includes("- a") && combined.includes("It was a night"), "combined markdown includes chapter content in order");
|
|
ok(combined.startsWith("# Test Book"), "combined markdown first line is book title");
|
|
|
|
const zip = await buildZip(dir);
|
|
ok(Buffer.isBuffer(zip), "buildZip returns a Buffer");
|
|
ok(zip.length > 0, "zip buffer is non-empty");
|
|
ok(zip.includes("book.md"), "zip contains book.md");
|
|
ok(zip.includes("title_page.md"), "zip contains title_page.md");
|
|
ok(zip.includes("manifest.json"), "zip contains manifest.json");
|
|
ok(zip.includes("chapters"), "zip contains a chapters/ folder");
|
|
ok(zip.slice(0, 2).toString("ascii") === "PK", "zip starts with PK signature");
|
|
cleanupExport();
|
|
|
|
cleanupProj();
|
|
cleanupChap();
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
|
|
console.log("\nChapter tests: " + (failures === 0 ? "ALL PASSED" : failures + " FAILED"));
|
|
process.exit(failures === 0 ? 0 : 1);
|