Add in-app search across chapters and switch to manual save
- Search: a 'Search' button in the sidebar footer (or Ctrl/Cmd+F) opens a modal that scans every chapter, lists matches grouped by chapter with highlighted snippets, and jumps to and scrolls the match on click. Includes a 'Match case' toggle. - Manual save: remove the per-keystroke auto-save; a chapter is only persisted via the new Save button or Ctrl/Cmd+S. Switching chapters or closing the book with unsaved edits now prompts to save first. - Extend the packaged self-test to exercise both features.
This commit is contained in:
parent
3052e335a3
commit
aad1a9332a
4 changed files with 459 additions and 16 deletions
182
tests/selftest-scenario.js
Normal file
182
tests/selftest-scenario.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Packaged self-test scenario. Electron Builder ships this file inside the
|
||||
// distributable (resources/app.asar/tests/selftest-scenario.js) and the main
|
||||
// process executes it in the renderer (see src/main/index.ts FOLIO_SELF_TEST)
|
||||
// only when the app is launched with FOLIO_SELF_TEST=1 and FOLIO_TEST_BOOK set.
|
||||
// Normal launches never touch this file. It drives the real packaged binary
|
||||
// through the same public API the UI uses and returns a summary JSON.
|
||||
(async () => {
|
||||
function withTimeout(p, ms, tag) {
|
||||
return Promise.race([
|
||||
p,
|
||||
new Promise((r) => setTimeout(() => r({ ok: false, timedOut: tag }), ms)),
|
||||
]);
|
||||
}
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
function waitFor(fn, timeout = 10000, interval = 100) {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
(function check() {
|
||||
let ok = false;
|
||||
try {
|
||||
ok = fn();
|
||||
} catch (e) {}
|
||||
if (ok) return resolve(true);
|
||||
if (Date.now() - start > timeout) return resolve(false);
|
||||
setTimeout(check, interval);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
return withTimeout(
|
||||
(async () => {
|
||||
const out = { ok: false, steps: {} };
|
||||
const step = (k, v) => {
|
||||
out.steps[k] = v;
|
||||
};
|
||||
const BOOK = __BOOKDIR_VALUE__;
|
||||
try {
|
||||
const api = window.folio;
|
||||
step("api", !!api);
|
||||
|
||||
const opened = await api.openBookPath(BOOK);
|
||||
step("openBook", opened);
|
||||
|
||||
const editorMounted = await waitFor(() =>
|
||||
!!document.querySelector("#editorHost .ProseMirror")
|
||||
);
|
||||
step("editorMounted", editorMounted);
|
||||
|
||||
const meta = await api.getBookMeta();
|
||||
step("meta", {
|
||||
title: meta && meta.title,
|
||||
chapters: meta && meta.chapters ? Object.keys(meta.chapters).length : 0,
|
||||
});
|
||||
|
||||
const chapters = await api.getChapterList();
|
||||
step("chapterList", Array.isArray(chapters) ? chapters.length : "n/a");
|
||||
|
||||
// Promote legacy chapters to the native model so DOCX/ODT have content.
|
||||
try {
|
||||
step("migrate", await api.migrateBook());
|
||||
} catch (e) {
|
||||
step("migrate", "err:" + e.message);
|
||||
}
|
||||
|
||||
// Theme switching must update the document attribute. Drive it through
|
||||
// the actual UI control (the theme <select>), not just the stored pref.
|
||||
const sel = document.getElementById("themeSelect");
|
||||
if (sel) {
|
||||
sel.value = "dark-mode";
|
||||
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await sleep(200);
|
||||
step("themeDark", document.documentElement.getAttribute("data-theme"));
|
||||
sel.value = "light";
|
||||
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await sleep(200);
|
||||
step("themeLight", document.documentElement.getAttribute("data-theme"));
|
||||
} else {
|
||||
step("themeDark", "no-theme-select");
|
||||
step("themeLight", "no-theme-select");
|
||||
}
|
||||
|
||||
// Chapter navigation: clicking a chapter row selects it.
|
||||
const row = document.querySelector("#chapterList li.chapter-row");
|
||||
if (row) {
|
||||
row.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await sleep(300);
|
||||
}
|
||||
step(
|
||||
"navActive",
|
||||
!!document.querySelector("#chapterList li.chapter-row.active")
|
||||
);
|
||||
|
||||
// Manual save: typing marks the chapter Unsaved; the Save button (or
|
||||
// Ctrl/Cmd+S) is required to persist. No auto-save should occur.
|
||||
const ed = window.__folioEditor;
|
||||
if (ed) {
|
||||
ed.commands.focus("end");
|
||||
ed.commands.insertContent(" manual-save-check");
|
||||
await sleep(150);
|
||||
const unsaved = document.getElementById("saveState");
|
||||
step("afterTypeUnsaved", unsaved ? unsaved.textContent : "n/a");
|
||||
const sb = document.getElementById("saveBtn");
|
||||
if (sb) {
|
||||
sb.click();
|
||||
await sleep(400);
|
||||
}
|
||||
const after = document.getElementById("saveState");
|
||||
step("afterSaveState", after ? after.textContent : "n/a");
|
||||
} else {
|
||||
step("afterTypeUnsaved", "no-editor");
|
||||
}
|
||||
|
||||
// Full Book view.
|
||||
const fb = document.getElementById("fullBookBtn");
|
||||
if (fb) {
|
||||
fb.click();
|
||||
await sleep(1200);
|
||||
}
|
||||
const fbv = document.getElementById("fullBookView");
|
||||
step("fullBookVisible", fbv ? !fbv.classList.contains("hidden") : false);
|
||||
step(
|
||||
"fullBookContent",
|
||||
!!document.querySelector("#fullBookHost .ProseMirror") ||
|
||||
!!document.querySelector("#fullBookHost *")
|
||||
);
|
||||
|
||||
// Search across all chapters (the new feature): open the search UI,
|
||||
// type a known term, confirm results, and jump to the first match.
|
||||
const sb = document.getElementById("searchBtn");
|
||||
if (sb) {
|
||||
sb.click();
|
||||
await sleep(120);
|
||||
const si = document.getElementById("searchInput");
|
||||
if (si) {
|
||||
si.value = "bold";
|
||||
si.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
await sleep(600);
|
||||
}
|
||||
step(
|
||||
"searchResults",
|
||||
document.querySelectorAll("#searchResults .search-result").length
|
||||
);
|
||||
const first = document.querySelector("#searchResults .search-result");
|
||||
if (first) {
|
||||
first.click();
|
||||
await sleep(400);
|
||||
}
|
||||
const cv = document.getElementById("chapterView");
|
||||
step("afterSearchChapterView", cv ? !cv.classList.contains("hidden") : false);
|
||||
} else {
|
||||
step("searchResults", "no-search-btn");
|
||||
}
|
||||
|
||||
// Exports (save paths come from FOLIO_TEST_* env in the main process).
|
||||
const ex = {};
|
||||
try {
|
||||
ex.pdf = await api.exportPdf(null);
|
||||
} catch (e) {
|
||||
ex.pdf = "err:" + e.message;
|
||||
}
|
||||
try {
|
||||
ex.docx = await api.exportDocx(null);
|
||||
} catch (e) {
|
||||
ex.docx = "err:" + e.message;
|
||||
}
|
||||
try {
|
||||
ex.odt = await api.exportOdt(null);
|
||||
} catch (e) {
|
||||
ex.odt = "err:" + e.message;
|
||||
}
|
||||
step("exports", ex);
|
||||
|
||||
out.ok = true;
|
||||
} catch (e) {
|
||||
out.error = String((e && e.stack) || e);
|
||||
}
|
||||
return out;
|
||||
})(),
|
||||
90000,
|
||||
"scenario"
|
||||
);
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue