222 lines
7.8 KiB
JavaScript
222 lines
7.8 KiB
JavaScript
// 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")
|
|
);
|
|
|
|
// Auto-save: typing marks the chapter Unsaved, then a debounced
|
|
// background save persists it automatically (no prompt, no manual
|
|
// Save required). Verify the indicator ends in "Saved".
|
|
const edSave = window.__folioEditor;
|
|
if (edSave) {
|
|
edSave.commands.focus("end");
|
|
edSave.commands.insertContent(" manual-save-check");
|
|
await sleep(150);
|
|
const unsaved = document.getElementById("saveState");
|
|
step("afterTypeUnsaved", unsaved ? unsaved.textContent : "n/a");
|
|
// Wait past the autosave debounce for the chapter to self-persist.
|
|
await sleep(1600);
|
|
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);
|
|
|
|
// Chapter delete + undo (snapshot restore).
|
|
try {
|
|
const before = (await api.getChapterList()).length;
|
|
const cc = await api.getChapterContent("ch-one");
|
|
const delc = await api.deleteChapter("ch-one");
|
|
const afterDel = (await api.getChapterList()).length;
|
|
const restc = await api.restoreChapter({
|
|
id: "ch-one",
|
|
title: "Book_The_Lab",
|
|
color: null,
|
|
content: cc,
|
|
index: 0,
|
|
});
|
|
const afterRest = (await api.getChapterList()).length;
|
|
const cc2 = await api.getChapterContent("ch-one");
|
|
step("chapterUndo", {
|
|
before,
|
|
afterDel,
|
|
afterRest,
|
|
restored: cc2 === cc,
|
|
ok: !!(delc && delc.ok) && !!(restc && restc.ok),
|
|
});
|
|
} catch (e) {
|
|
step("chapterUndoErr", String(e));
|
|
}
|
|
|
|
// New book auto-creates a folder inside Documents; delete then undo.
|
|
const nb = await api.newBookNamed("Folio Selftest Temp");
|
|
step("newBookNamed", {
|
|
ok: !!(nb && nb.path && nb.title),
|
|
path: nb && nb.path,
|
|
title: nb && nb.title,
|
|
});
|
|
if (nb && nb.path) {
|
|
const del = await api.deleteBook(nb.path);
|
|
step("deleteBook", del && del.ok ? "ok" : del && del.error);
|
|
if (del && del.ok) {
|
|
const rest = await api.restoreBook(nb.path);
|
|
step("restoreBook", rest && rest.ok ? "ok" : rest && rest.error);
|
|
}
|
|
}
|
|
|
|
out.ok = true;
|
|
} catch (e) {
|
|
out.error = String((e && e.stack) || e);
|
|
}
|
|
return out;
|
|
})(),
|
|
90000,
|
|
"scenario"
|
|
);
|
|
})();
|