diff --git a/src/renderer/book.ts b/src/renderer/book.ts index eb60941..c05b990 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -845,9 +845,20 @@ export function initBook(api: FolioAPI) { if (editorView.hasFocus) redo(editorView); else document.execCommand("redo"); }); + api.onSelectAllRequested(() => { + if (!fullBookView.classList.contains("hidden")) { + fullBookEditor.focus(); + selectAll(fullBookEditor); + } else if (editorView.hasFocus) { + selectAll(editorView); + } else { + document.execCommand("selectAll"); + } + }); setChapterButtons(false); void renderRecents(); (window as any).__folioEditor = editorView; + (window as any).__folioFullBookEditor = fullBookEditor; } diff --git a/tests/run-e2e.sh b/tests/run-e2e.sh index 492d33c..fbbc9e7 100755 --- a/tests/run-e2e.sh +++ b/tests/run-e2e.sh @@ -5,7 +5,9 @@ # 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);') +BOOK2=$(node -e 'const fs=require("fs"),path=require("path"),os=require("os");const d=fs.mkdtempSync(path.join(os.tmpdir(),"folio-e2e2-"));fs.mkdirSync(path.join(d,"chapters"),{recursive:true});fs.writeFileSync(path.join(d,"folio.json"),JSON.stringify({app:"folio",version:1,title:"E2E Book 2",chapterOrder:[],chapters:{}}));process.stdout.write(d);') export FOLIO_SELF_TEST=1 export FOLIO_TEST_BOOK="$BOOK" -trap 'rm -rf "$BOOK"' EXIT +export FOLIO_TEST_BOOK2="$BOOK2" +trap 'rm -rf "$BOOK" "$BOOK2"' EXIT xvfb-run -a electron . diff --git a/tests/selftest-scenario.js b/tests/selftest-scenario.js index 269220a..5f530ab 100644 --- a/tests/selftest-scenario.js +++ b/tests/selftest-scenario.js @@ -2,6 +2,7 @@ Injected only when FOLIO_SELF_TEST=1. Exercises the full renderer -> preload -> IPC -> chapters chain. */ const BOOK_DIR = __BOOKDIR_VALUE__; +const BOOK2_DIR = __BOOKDIR2_VALUE__; (async () => { const out = {}; try { @@ -212,6 +213,62 @@ const BOOK_DIR = __BOOKDIR_VALUE__; (await window.folio.getChapterContent(typedId)).includes("**Hello**"); } + // --- full book view (fresh second book with known chapter contents) --- + await window.folio.openBookPath(BOOK2_DIR); + await new Promise((r) => setTimeout(r, 400)); + const fb = window.__folioFullBookEditor; + const alphaId = (await window.folio.createChapter("Alpha")).id; + const betaId = (await window.folio.createChapter("Beta")).id; + const gammaId = (await window.folio.createChapter("Gamma")).id; + await new Promise((r) => setTimeout(r, 300)); + await window.folio.saveChapter(alphaId, "# Alpha\n\nAlpha body text"); + await window.folio.saveChapter(betaId, "Beta body without heading"); + await window.folio.saveChapter(gammaId, "# Gamma's Own Title\n\nGamma text"); + await new Promise((r) => setTimeout(r, 300)); + + document.getElementById("fullBookBtn").click(); + await new Promise((r) => setTimeout(r, 300)); + + const expectedAssembled = + "# Alpha\n\nAlpha body text\n\n# Beta\n\nBeta body without heading\n\n# Gamma\n\n# Gamma's Own Title\n\nGamma text"; + out.fullBookShown = + !document.getElementById("fullBookView").classList.contains("hidden") && + document.getElementById("chapterView").classList.contains("hidden") && + document.getElementById("fullBookBtn").classList.contains("active") && + !document.getElementById("fullBookNav").classList.contains("hidden"); + const assembled = fb.state.doc.toString(); + out.assembled = assembled; + out.assembledOk = assembled === expectedAssembled; + const words = expectedAssembled.trim().split(/\s+/).length; + const statsEl = document.getElementById("fullBookStats"); + out.fullBookStatsText = statsEl.textContent; + out.fullBookStatsOk = + statsEl.textContent === `words: ${words} ยท chars: ${expectedAssembled.length}`; + + document.getElementById("copyAllBtn").click(); + await new Promise((r) => setTimeout(r, 200)); + const clip = await window.folio.getClipboardText(); + out.clipboardLen = clip.length; + out.copyAllOk = clip === expectedAssembled; + out.copyStatusShown = document.getElementById("fullBookStatus").textContent === "Full book copied"; + + // Ctrl+A (routed through the renderer listener) selects the whole read-only doc + fb.focus(); + fb.contentDOM.dispatchEvent( + new KeyboardEvent("keydown", { key: "a", ctrlKey: true, bubbles: true, cancelable: true }) + ); + const sel = fb.state.selection.main; + out.fullBookSelectAllOk = + fb.state.doc.length > 0 && sel.from === 0 && sel.to === fb.state.doc.length; + + // back to normal chapter editing still works + document.querySelector("#chapterList .chapter-row").click(); + await new Promise((r) => setTimeout(r, 250)); + out.backToChapterOk = + document.getElementById("fullBookView").classList.contains("hidden") && + !document.getElementById("chapterView").classList.contains("hidden") && + !document.getElementById("fullBookBtn").classList.contains("active"); + const okFlags = [ "dndMoved", "kbMoved", @@ -233,6 +290,13 @@ const BOOK_DIR = __BOOKDIR_VALUE__; "tableOk", "painterOk", "formattingAutosaved", + "fullBookShown", + "assembledOk", + "fullBookStatsOk", + "copyAllOk", + "copyStatusShown", + "fullBookSelectAllOk", + "backToChapterOk", ]; out.all = okFlags.every((f) => out[f] === true); } catch (e) {