Stage 3: chapter management — create/rename/delete/duplicate, sidebar list, select, read-only preview, word count, IPC + tests + e2e self-test

This commit is contained in:
avi 2026-08-16 14:34:34 -05:00
commit d3ab261346
11 changed files with 811 additions and 134 deletions

113
tests/run-chapters-test.mjs Normal file
View file

@ -0,0 +1,113 @@
// Standalone test for chapter CRUD logic.
// Run with: node tests/run-chapters-test.mjs
import esbuild from "esbuild";
import fs from "fs";
import os from "os";
import path from "path";
import url from "url";
const projOut = path.join(os.tmpdir(), `folio-project-${process.pid}.mjs`);
const chapOut = path.join(os.tmpdir(), `folio-chapters-${process.pid}.mjs`);
await esbuild.build({
entryPoints: [path.resolve("src/main/project.ts")],
bundle: true,
platform: "node",
format: "esm",
outfile: projOut,
write: true,
logLevel: "silent",
});
await esbuild.build({
entryPoints: [path.resolve("src/main/chapters.ts")],
bundle: true,
platform: "node",
format: "esm",
outfile: chapOut,
write: true,
logLevel: "silent",
});
const project = await import(url.pathToFileURL(projOut).href);
const chapters = await import(url.pathToFileURL(chapOut).href);
const { createBook, loadBook, saveMeta, hasBook } = project;
const {
listChaptersInOrder,
getChapterContent,
createChapter,
renameChapter,
deleteChapter,
duplicateChapter,
reorderChapters,
} = 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");
// 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");
// empty title throws
threw = false;
try { createChapter(dir, " "); } catch { threw = true; }
ok(threw, "createChapter rejects empty title");
fs.rmSync(projOut, { force: true });
fs.rmSync(chapOut, { force: true });
fs.rmSync(dir, { recursive: true, force: true });
console.log("\nChapter tests: " + (failures === 0 ? "ALL PASSED" : failures + " FAILED"));
process.exit(failures === 0 ? 0 : 1);