Fix Full Book assembly to always render from canonical chapter metadata
Full Book now assembles from a fresh read of the canonical book metadata (chapter order + chapter entries) instead of the renderer's in-memory chapter array, so it always contains every chapter in the current order and can never render from stale or partially-loaded state. The chapter sidebar and Full Book share the same single source of truth. Also refresh Full Book whenever book metadata loads, and extend the e2e scenario to cover reordering to a new order, order persistence across reopen, and delete/rename/new-empty chapters all being reflected.
This commit is contained in:
parent
46eca18cdf
commit
6b1a9da5cc
2 changed files with 104 additions and 4 deletions
|
|
@ -402,6 +402,7 @@ export function initBook(api: FolioAPI) {
|
||||||
if (ordered.length) showPlaceholder();
|
if (ordered.length) showPlaceholder();
|
||||||
else showPlaceholder();
|
else showPlaceholder();
|
||||||
}
|
}
|
||||||
|
await refreshFullBook();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshMeta() {
|
async function refreshMeta() {
|
||||||
|
|
@ -410,7 +411,6 @@ export function initBook(api: FolioAPI) {
|
||||||
selectedId = null;
|
selectedId = null;
|
||||||
showPlaceholder();
|
showPlaceholder();
|
||||||
}
|
}
|
||||||
await refreshFullBook();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChapterList() {
|
function renderChapterList() {
|
||||||
|
|
@ -501,11 +501,19 @@ export function initBook(api: FolioAPI) {
|
||||||
return false;
|
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<string> {
|
async function assembleFullBook(): Promise<string> {
|
||||||
|
const res = await api.getBookMeta();
|
||||||
|
if (!res || typeof res !== "object" || "error" in res) return "";
|
||||||
|
const book = res as BookMeta;
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
for (const ch of ordered) {
|
for (const id of book.chapterOrder) {
|
||||||
const res = await api.getChapterContent(ch.id);
|
const ch = book.chapters[id];
|
||||||
const content = typeof res === "string" ? res : "";
|
if (!ch) continue;
|
||||||
|
const contentRes = await api.getChapterContent(ch.id);
|
||||||
|
const content = typeof contentRes === "string" ? contentRes : "";
|
||||||
const body = content.replace(/^\n+/, "").replace(/\s+$/, "");
|
const body = content.replace(/^\n+/, "").replace(/\s+$/, "");
|
||||||
const block: string[] = [];
|
const block: string[] = [];
|
||||||
if (!chapterHasOwnHeading(content, ch.title)) {
|
if (!chapterHasOwnHeading(content, ch.title)) {
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,89 @@ const BOOK2_DIR = __BOOKDIR2_VALUE__;
|
||||||
!document.getElementById("chapterView").classList.contains("hidden") &&
|
!document.getElementById("chapterView").classList.contains("hidden") &&
|
||||||
!document.getElementById("fullBookBtn").classList.contains("active");
|
!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 = [
|
const okFlags = [
|
||||||
"dndMoved",
|
"dndMoved",
|
||||||
"kbMoved",
|
"kbMoved",
|
||||||
|
|
@ -297,6 +380,15 @@ const BOOK2_DIR = __BOOKDIR2_VALUE__;
|
||||||
"copyStatusShown",
|
"copyStatusShown",
|
||||||
"fullBookSelectAllOk",
|
"fullBookSelectAllOk",
|
||||||
"backToChapterOk",
|
"backToChapterOk",
|
||||||
|
"reorderOk",
|
||||||
|
"fbAfterReorderOk",
|
||||||
|
"fbHasAllContent",
|
||||||
|
"fbNoStale",
|
||||||
|
"reopenPersists",
|
||||||
|
"deleteOk",
|
||||||
|
"fbAfterDeleteOk",
|
||||||
|
"fbAfterRenameOk",
|
||||||
|
"fbEmptyOk",
|
||||||
];
|
];
|
||||||
out.all = okFlags.every((f) => out[f] === true);
|
out.all = okFlags.every((f) => out[f] === true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue