English spellcheck suggestions with Add-to-dictionary in the native context menu plus a Settings toggle. Typewriter caret-centering mode. OS file associations for .md/.txt and Open File wording. Updater: release URL surfaced, single progress listener, two-byte tarball check, installer wait guard. Search/installer test suites registered; chapter-list layout polish; ignore KDE .directory files.
98 lines
5.6 KiB
JavaScript
98 lines
5.6 KiB
JavaScript
import esbuild from "esbuild";
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import url from "url";
|
|
|
|
await esbuild.build({
|
|
entryPoints: ["src/main/update.ts"],
|
|
bundle: true, platform: "node", format: "esm",
|
|
outfile: "/tmp/folio-smoke-update.mjs", external: ["electron"], logLevel: "silent",
|
|
});
|
|
await esbuild.build({
|
|
entryPoints: ["src/main/import-md.ts"],
|
|
bundle: true, platform: "node", format: "esm",
|
|
outfile: "/tmp/folio-smoke-import.mjs", external: ["electron"], logLevel: "silent",
|
|
});
|
|
const u = await import(url.pathToFileURL("/tmp/folio-smoke-update.mjs").href);
|
|
const m = await import(url.pathToFileURL("/tmp/folio-smoke-import.mjs").href);
|
|
|
|
let fails = 0;
|
|
const assert = (c, msg) => { if (!c) { console.error("FAIL -", msg); fails++; } else console.log("ok -", msg); };
|
|
|
|
assert(u.isNewer("0.2.0", "0.1.0"), "0.2.0 newer than 0.1.0");
|
|
assert(!u.isNewer("v0.1.0", "0.1.0"), "same version not newer (v-prefix tolerated)");
|
|
assert(u.isNewer("0.1.10", "0.1.9"), "numeric compare not lexicographic");
|
|
|
|
const rel = u.parseRelease({
|
|
tag_name: "v0.2.0",
|
|
assets: [
|
|
{ name: "Folio-0.2.0-linux.tar.gz", browser_download_url: "https://git.atitlan.io/avi/Folio/releases/download/v0.2.0/Folio-0.2.0-linux.tar.gz" },
|
|
{ name: "Folio-0.1.0.AppImage", browser_download_url: "https://git.atitlan.io/x/Folio.AppImage" },
|
|
],
|
|
});
|
|
assert(rel.version === "0.2.0", "release version parsed");
|
|
assert(u.pickAsset(rel.assets).name.includes("linux"), "picks linux tar.gz over AppImage");
|
|
assert(u.isTrustedDownloadUrl("https://git.atitlan.io/a/t.tar.gz", "https://git.atitlan.io"), "same-host trusted");
|
|
assert(!u.isTrustedDownloadUrl("https://evil.io/t.tar.gz", "https://git.atitlan.io"), "cross-host rejected");
|
|
let threw = false;
|
|
try { u.parseRelease({ message: "not found" }); } catch { threw = true; }
|
|
assert(threw, "error payload without tag throws");
|
|
|
|
const sh = u.buildInstallerScript({ pid: 1234, tarball: "/tmp/it's.tar.gz", installDir: "/home/a/.local/share/Folio", relaunch: "/home/a/.local/bin/folio" });
|
|
assert(sh.includes(`'/tmp/it'\\''s.tar.gz'`), "shQuote escapes embedded single quote");
|
|
assert(sh.includes("kill -0"), "installer waits for app pid");
|
|
assert(sh.includes("-xzf"), "installer extracts gz");
|
|
|
|
// Real Gitea release-JSON shape (tag_name/html_url/body/assets with sizes).
|
|
const gitea = u.parseRelease({
|
|
id: 5,
|
|
tag_name: "v0.2.0",
|
|
html_url: "https://git.atitlan.io/avi/Folio/releases/tag/v0.2.0",
|
|
body: "Bug fixes",
|
|
assets: [
|
|
{ name: "Folio-0.2.0-linux-x86_64.tar.gz", browser_download_url: "https://git.atitlan.io/avi/Folio/releases/download/v0.2.0/Folio-0.2.0-linux-x86_64.tar.gz", size: 123456 },
|
|
{ name: "Folio-0.2.0-linux-amd64.deb", browser_download_url: "https://git.atitlan.io/avi/Folio/releases/download/v0.2.0/Folio-0.2.0-linux-amd64.deb", size: 111 },
|
|
{ name: "Folio-0.2.0.AppImage", browser_download_url: "https://git.atitlan.io/avi/Folio/releases/download/v0.2.0/Folio-0.2.0.AppImage", size: 222 },
|
|
],
|
|
});
|
|
assert(gitea.version === "0.2.0", "gitea tag parsed");
|
|
assert(gitea.url === "https://git.atitlan.io/avi/Folio/releases/tag/v0.2.0", "gitea release url kept");
|
|
assert(gitea.notes === "Bug fixes", "gitea notes kept");
|
|
assert(u.pickAsset(gitea.assets).name.endsWith(".tar.gz"), "gitea: tar.gz picked over deb/AppImage");
|
|
assert(u.isTrustedDownloadUrl(gitea.assets[0].browser_download_url, "https://git.atitlan.io"), "gitea asset url trusted");
|
|
|
|
// No installable archive cases.
|
|
assert(u.pickAsset([]) === null, "empty assets -> no pick");
|
|
const debOnly = u.parseRelease({ tag_name: "v0.3.0", assets: [{ name: "Folio.deb", browser_download_url: "https://git.atitlan.io/x/Folio.deb" }] });
|
|
assert(u.pickAsset(debOnly.assets) === null, "deb-only release -> no pick (tar.gz required)");
|
|
|
|
// Version edge cases.
|
|
assert(!u.isNewer("0.1.0", "0.1.0"), "same version not an update");
|
|
assert(!u.isNewer("0.1.0", "0.2.0"), "older release not an update");
|
|
assert(u.compareVersions("0.1.2-rc1", "0.1.2") === 0, "rc suffix ignored in compare");
|
|
assert(u.parseVersion("nope") === null, "unparseable version -> null");
|
|
assert(!u.isTrustedDownloadUrl("http://git.atitlan.io/a.tar.gz", "https://git.atitlan.io"), "http vs https rejected");
|
|
assert(!u.isTrustedDownloadUrl("not a url", "https://git.atitlan.io"), "malformed url rejected");
|
|
|
|
assert(m.mdTitle("# My Heading\nbody", "file") === "My Heading", "title from H1");
|
|
assert(m.mdTitle("no heading", "file") === "file", "title falls back to filename stem");
|
|
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "imp-"));
|
|
fs.writeFileSync(path.join(dir, "a.md"), "# Alpha\n\nhello world");
|
|
fs.writeFileSync(path.join(dir, "b.md"), "plain text no heading");
|
|
const r1 = m.importMarkdownFiles([path.join(dir, "a.md"), path.join(dir, "b.md")], path.join(dir, "out"));
|
|
assert(r1[0].ok && r1[0].title === "Alpha", "import a.md -> book 'Alpha'");
|
|
assert(r1[1].ok && r1[1].title === "b", "import b.md -> book 'b'");
|
|
assert(fs.readFileSync(path.join(dir, "a.md"), "utf-8") === "# Alpha\n\nhello world", "original file untouched");
|
|
const r2 = m.importMarkdownFiles([path.join(dir, "a.md")], path.join(dir, "out"));
|
|
assert(r2[0].ok && r2[0].bookPath.endsWith("Alpha 2"), "name collision gets numeric suffix");
|
|
const pj = JSON.parse(fs.readFileSync(path.join(dir, "out", "Alpha", "folio.json"), "utf-8"));
|
|
const entry = pj.chapters[pj.chapterOrder[0]];
|
|
const ch = fs.readFileSync(path.join(dir, "out", "Alpha", entry.file), "utf-8");
|
|
assert(ch.includes("hello world"), "chapter content copied into book");
|
|
const r3 = m.importMarkdownFiles(["/nonexistent/zz.md"], path.join(dir, "out2"));
|
|
assert(!r3[0].ok && r3[0].error, "missing file reports error, no crash");
|
|
|
|
console.log(fails ? `SMOKE: ${fails} FAILURES` : "SMOKE: ALL PASSED");
|
|
process.exit(fails ? 1 : 0);
|