diff --git a/src/renderer/book.ts b/src/renderer/book.ts index c05b990..2aa1eb4 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -402,6 +402,7 @@ export function initBook(api: FolioAPI) { if (ordered.length) showPlaceholder(); else showPlaceholder(); } + await refreshFullBook(); } async function refreshMeta() { @@ -410,7 +411,6 @@ export function initBook(api: FolioAPI) { selectedId = null; showPlaceholder(); } - await refreshFullBook(); } function renderChapterList() { @@ -501,11 +501,19 @@ export function initBook(api: FolioAPI) { return false; } + // Assemble the whole book from the canonical book metadata (the same source + // the chapter sidebar uses) so Full Book always reflects the current chapter + // set and order, regardless of any in-memory renderer state. async function assembleFullBook(): Promise { + const res = await api.getBookMeta(); + if (!res || typeof res !== "object" || "error" in res) return ""; + const book = res as BookMeta; const parts: string[] = []; - for (const ch of ordered) { - const res = await api.getChapterContent(ch.id); - const content = typeof res === "string" ? res : ""; + for (const id of book.chapterOrder) { + const ch = book.chapters[id]; + if (!ch) continue; + const contentRes = await api.getChapterContent(ch.id); + const content = typeof contentRes === "string" ? contentRes : ""; const body = content.replace(/^\n+/, "").replace(/\s+$/, ""); const block: string[] = []; if (!chapterHasOwnHeading(content, ch.title)) { diff --git a/tests/selftest-scenario.js b/tests/selftest-scenario.js index 5f530ab..1a87ae5 100644 --- a/tests/selftest-scenario.js +++ b/tests/selftest-scenario.js @@ -269,6 +269,89 @@ const BOOK2_DIR = __BOOKDIR2_VALUE__; !document.getElementById("chapterView").classList.contains("hidden") && !document.getElementById("fullBookBtn").classList.contains("active"); + // --- requirement matrix: reorder, persistence, edit ops reflect in Full Book --- + const rowFor = (id) => + Array.from(document.querySelectorAll("#chapterList .chapter-row")).find((r) => r.dataset.id === id); + const moveUp = async (id) => { + rowFor(id).dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowUp", altKey: true, bubbles: true }) + ); + await new Promise((r) => setTimeout(r, 250)); + }; + + // current order: [Alpha, Beta, Gamma]; move Gamma to front -> [Gamma, Alpha, Beta] + rowFor(gammaId).click(); + await new Promise((r) => setTimeout(r, 200)); + await moveUp(gammaId); + await moveUp(gammaId); + const orderReordered = (await window.folio.getBookMeta()).chapterOrder; + out.orderReordered = orderReordered; + out.reorderOk = + orderReordered[0] === gammaId && orderReordered[1] === alphaId && orderReordered[2] === betaId; + + // Full Book follows the new chapter order and keeps every chapter's content + document.getElementById("fullBookBtn").click(); + await new Promise((r) => setTimeout(r, 300)); + const assembledGAB = fb.state.doc.toString(); + out.fbAfterReorder = assembledGAB; + out.fbAfterReorderOk = + assembledGAB.startsWith("# Gamma\n\n# Gamma's Own Title") && + assembledGAB.indexOf("# Alpha") > assembledGAB.indexOf("# Gamma") && + assembledGAB.indexOf("# Beta") > assembledGAB.indexOf("# Alpha"); + out.fbHasAllContent = + assembledGAB.includes("Gamma text") && + assembledGAB.includes("Alpha body text") && + assembledGAB.includes("Beta body without heading"); + + // switching between an individual chapter and Full Book never shows stale data + document.querySelector("#chapterList .chapter-row").click(); + await new Promise((r) => setTimeout(r, 200)); + document.getElementById("fullBookBtn").click(); + await new Promise((r) => setTimeout(r, 300)); + out.fbNoStale = fb.state.doc.toString() === assembledGAB; + + // closing and reopening the book preserves the chapter order + await window.folio.openBookPath(BOOK2_DIR); + await new Promise((r) => setTimeout(r, 400)); + const orderReopened = (await window.folio.getBookMeta()).chapterOrder; + out.orderAfterReopen = orderReopened; + out.reopenPersists = JSON.stringify(orderReopened) === JSON.stringify(orderReordered); + + // deleting a chapter updates the sidebar and Full Book + rowFor(alphaId).querySelector('[data-act="del"]').click(); + await new Promise((r) => setTimeout(r, 150)); + document.getElementById("modalOk").click(); + await new Promise((r) => setTimeout(r, 300)); + out.orderAfterDelete = (await window.folio.getBookMeta()).chapterOrder; + out.deleteOk = !(await window.folio.getBookMeta()).chapterOrder.includes(alphaId); + document.getElementById("fullBookBtn").click(); + await new Promise((r) => setTimeout(r, 300)); + const assembledAfterDelete = fb.state.doc.toString(); + out.fbAfterDelete = assembledAfterDelete; + out.fbAfterDeleteOk = + !assembledAfterDelete.includes("Alpha body text") && assembledAfterDelete.includes("Gamma text"); + + // renaming a chapter updates its Full Book heading + rowFor(betaId).querySelector('[data-act="rename"]').click(); + await new Promise((r) => setTimeout(r, 150)); + document.getElementById("modalInput").value = "Beta Renamed"; + document.getElementById("modalOk").click(); + await new Promise((r) => setTimeout(r, 300)); + const assembledAfterRename = fb.state.doc.toString(); + out.fbAfterRename = assembledAfterRename; + out.fbAfterRenameOk = + assembledAfterRename.includes("# Beta Renamed") && + !assembledAfterRename.includes("# Beta\n\nBeta body without heading"); + + // a newly created empty chapter appears in Full Book with just its heading + const emptyId = (await window.folio.createChapter("Empty")).id; + await new Promise((r) => setTimeout(r, 300)); + document.getElementById("fullBookBtn").click(); + await new Promise((r) => setTimeout(r, 300)); + const assembledWithEmpty = fb.state.doc.toString(); + out.fbWithEmpty = assembledWithEmpty; + out.fbEmptyOk = assembledWithEmpty.includes("# Empty") && assembledWithEmpty.includes("Beta Renamed"); + const okFlags = [ "dndMoved", "kbMoved", @@ -297,6 +380,15 @@ const BOOK2_DIR = __BOOKDIR2_VALUE__; "copyStatusShown", "fullBookSelectAllOk", "backToChapterOk", + "reorderOk", + "fbAfterReorderOk", + "fbHasAllContent", + "fbNoStale", + "reopenPersists", + "deleteOk", + "fbAfterDeleteOk", + "fbAfterRenameOk", + "fbEmptyOk", ]; out.all = okFlags.every((f) => out[f] === true); } catch (e) {