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:
parent
2862c433bd
commit
d3ab261346
11 changed files with 811 additions and 134 deletions
113
tests/run-chapters-test.mjs
Normal file
113
tests/run-chapters-test.mjs
Normal 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);
|
||||
11
tests/run-e2e.sh
Executable file
11
tests/run-e2e.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env sh
|
||||
# End-to-end self-test runner (Linux, requires xvfb-run for headless).
|
||||
# Creates a throwaway book, opens it via folio:openBookPath (no dialogs),
|
||||
# runs the full chapter CRUD scenario through the real renderer -> IPC -> chapters
|
||||
# chain, prints a JSON summary, and quits. Inert unless run explicitly.
|
||||
set -e
|
||||
BOOK=$(node -e 'const fs=require("fs"),path=require("path"),os=require("os");const d=fs.mkdtempSync(path.join(os.tmpdir(),"folio-e2e-"));fs.mkdirSync(path.join(d,"chapters"),{recursive:true});fs.writeFileSync(path.join(d,"folio.json"),JSON.stringify({app:"folio",version:1,title:"E2E Book",chapterOrder:[],chapters:{}}));process.stdout.write(d);')
|
||||
export FOLIO_SELF_TEST=1
|
||||
export FOLIO_TEST_BOOK="$BOOK"
|
||||
trap 'rm -rf "$BOOK"' EXIT
|
||||
xvfb-run -a electron .
|
||||
39
tests/selftest-scenario.js
Normal file
39
tests/selftest-scenario.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/* Selftest scenario - runs inside the renderer via executeJavaScript.
|
||||
Injected only when FOLIO_SELF_TEST=1. Exercises the full
|
||||
renderer -> preload -> IPC -> chapters chain. */
|
||||
const BOOK_DIR = __BOOKDIR_VALUE__;
|
||||
(async () => {
|
||||
const out = {};
|
||||
try {
|
||||
out.folioType = typeof window.folio;
|
||||
out.recentsCount = (await window.folio.getRecents()).length;
|
||||
out.openBook = await window.folio.openBookPath(BOOK_DIR);
|
||||
out.metaTitle = (await window.folio.getBookMeta()).title;
|
||||
|
||||
const created = await window.folio.createChapter("Introduction");
|
||||
out.created = created && !created.error ? { id: created.id, title: created.title } : created;
|
||||
const cid = created.id;
|
||||
out.contentEmpty = await window.folio.getChapterContent(cid);
|
||||
out.listLen1 = (await window.folio.getChapterList()).length;
|
||||
|
||||
out.renamed = await window.folio.renameChapter(cid, "Intro");
|
||||
const dup = await window.folio.duplicateChapter(cid);
|
||||
out.dup = dup && !dup.error ? { id: dup.id, title: dup.title } : dup;
|
||||
const did = dup.id;
|
||||
out.listLen2 = (await window.folio.getChapterList()).length;
|
||||
|
||||
out.deleted = await window.folio.deleteChapter(cid);
|
||||
const remaining = (await window.folio.getChapterList())[0];
|
||||
out.listLen3 = (await window.folio.getChapterList()).length;
|
||||
|
||||
out.reorderOk = await window.folio.reorderChapters([remaining.id]);
|
||||
out.reorderErr = await window.folio.reorderChapters(["bogus-id"]);
|
||||
|
||||
out.dupContent = await window.folio.getChapterContent(did);
|
||||
|
||||
out.all = true;
|
||||
} catch (e) {
|
||||
out.error = String((e && e.message) || e);
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue