diff --git a/tests/helpers/bundle.mjs b/tests/helpers/bundle.mjs new file mode 100644 index 0000000..273faf8 --- /dev/null +++ b/tests/helpers/bundle.mjs @@ -0,0 +1,55 @@ +// Shared test-harness bundler. +// +// Every run-*-test.mjs used to hand-roll the same esbuild boilerplate: +// write temp entry (optional) -> esbuild.build(bundle/node/esm/silent) +// -> dynamic import of the artifact -> rmSync cleanup. +// This module is that sequence, parameterized. Harnesses call it once (or +// more, like the chapters harness's three bundles) and get back the loaded +// module plus a scoped cleanup function. +import esbuild from "esbuild"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import url from "url"; + +export async function bundleForTest({ + // Unique-ish name; also names the temp artifacts (and the generated entry + // file when entrySource is used). + name, + // Module paths to bundle directly (relative to the project root or absolute). + entryPoints, + // Alternative to entryPoints: inline TS re-export text, written to a temp + // entry file next to the project root (deleted right after the build). + entrySource, + format = "esm", + external = [], +} = {}) { + const ext = format === "cjs" ? "cjs" : "mjs"; + const out = path.join(os.tmpdir(), `${name}-${process.pid}.${ext}`); + let tempEntry = null; + let eps = entryPoints ?? []; + if (entrySource !== undefined) { + tempEntry = path.resolve(`.${name}-entry.ts`); + fs.writeFileSync(tempEntry, entrySource); + eps = [tempEntry]; + } + await esbuild.build({ + entryPoints: eps.map((p) => path.resolve(p)), + bundle: true, + platform: "node", + format, + outfile: out, + external, + write: true, + logLevel: "silent", + }); + if (tempEntry) fs.rmSync(tempEntry, { force: true }); + const mod = await import(url.pathToFileURL(out).href); + return { + mod, + // Removes only this call's bundled artifact. + cleanupBundle() { + fs.rmSync(out, { force: true }); + }, + }; +} diff --git a/tests/run-chapters-test.mjs b/tests/run-chapters-test.mjs index c5dfe6d..01f439c 100644 --- a/tests/run-chapters-test.mjs +++ b/tests/run-chapters-test.mjs @@ -1,34 +1,18 @@ // Standalone test for chapter CRUD logic. // Run with: node tests/run-chapters-test.mjs -import esbuild from "esbuild"; import fs from "fs"; import os from "os"; import path from "path"; -import url from "url"; +import { bundleForTest } from "./helpers/bundle.mjs"; -const projOut = path.join(os.tmpdir(), `folio-project-${process.pid}.mjs`); -const chapOut = path.join(os.tmpdir(), `folio-chapters-${process.pid}.mjs`); -await esbuild.build({ - entryPoints: [path.resolve("src/main/project.ts")], - bundle: true, - platform: "node", - format: "esm", - outfile: projOut, - write: true, - logLevel: "silent", +const { mod: project, cleanupBundle: cleanupProj } = await bundleForTest({ + name: "folio-project", + entryPoints: ["src/main/project.ts"], }); -await esbuild.build({ - entryPoints: [path.resolve("src/main/chapters.ts")], - bundle: true, - platform: "node", - format: "esm", - outfile: chapOut, - write: true, - logLevel: "silent", +const { mod: chapters, cleanupBundle: cleanupChap } = await bundleForTest({ + name: "folio-chapters", + entryPoints: ["src/main/chapters.ts"], }); - -const project = await import(url.pathToFileURL(projOut).href); -const chapters = await import(url.pathToFileURL(chapOut).href); const { createBook, loadBook, saveMeta, hasBook } = project; const { listChaptersInOrder, @@ -145,18 +129,12 @@ ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 3, 0)) === JSON.stringify([" ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 1, 1)) === JSON.stringify(["a", "b", "c", "d"]), "no-op on same index"); // ---- export (combineMarkdown + buildZip) ---- -const exportOut = path.join(os.tmpdir(), `folio-export-${process.pid}.cjs`); -await esbuild.build({ - entryPoints: [path.resolve("src/main/export.ts")], - bundle: true, - platform: "node", +const { mod: exportMod, cleanupBundle: cleanupExport } = await bundleForTest({ + name: "folio-export", + entryPoints: ["src/main/export.ts"], format: "cjs", - outfile: exportOut, - write: true, - logLevel: "silent", }); -const exportMod = (await import(url.pathToFileURL(exportOut).href)).default; -const { combineMarkdown, buildZip } = exportMod; +const { combineMarkdown, buildZip } = exportMod.default; const b1 = createChapter(dir, "Intro"); setChapterContent(dir, b1.id, "# Intro\n\nHello world\n\n- a\n- b"); @@ -177,10 +155,10 @@ ok(zip.includes("title_page.md"), "zip contains title_page.md"); ok(zip.includes("manifest.json"), "zip contains manifest.json"); ok(zip.includes("chapters"), "zip contains a chapters/ folder"); ok(zip.slice(0, 2).toString("ascii") === "PK", "zip starts with PK signature"); -fs.rmSync(exportOut, { force: true }); +cleanupExport(); -fs.rmSync(projOut, { force: true }); -fs.rmSync(chapOut, { force: true }); +cleanupProj(); +cleanupChap(); fs.rmSync(dir, { recursive: true, force: true }); console.log("\nChapter tests: " + (failures === 0 ? "ALL PASSED" : failures + " FAILED")); diff --git a/tests/run-editor-test.mjs b/tests/run-editor-test.mjs index 4d6e410..148c67e 100644 --- a/tests/run-editor-test.mjs +++ b/tests/run-editor-test.mjs @@ -1,11 +1,10 @@ // Runner for the Tiptap editor test. Sets up a jsdom DOM, then dynamically // imports the bundled editor module so Tiptap sees a browser-like environment. -import esbuild from "esbuild"; import fs from "fs"; import os from "os"; import path from "path"; -import url from "url"; import { JSDOM } from "jsdom"; +import { bundleForTest } from "./helpers/bundle.mjs"; const dom = new JSDOM("
", { pretendToBeVisual: true, @@ -24,19 +23,10 @@ g.DOMParser = window.DOMParser; g.requestAnimationFrame = (cb) => setTimeout(cb, 0); g.cancelAnimationFrame = (id) => clearTimeout(id); -const out = path.join(os.tmpdir(), `folio-editor-${process.pid}.mjs`); -const entry = path.resolve("tests/.folio-editor-test-entry.ts"); -await esbuild.build({ - entryPoints: [entry], - bundle: true, - platform: "node", - format: "esm", - outfile: out, - write: true, - logLevel: "silent", +const { mod, cleanupBundle } = await bundleForTest({ + name: "folio-editor", + entryPoints: ["tests/.folio-editor-test-entry.ts"], }); - -const mod = await import(url.pathToFileURL(out).href); const { createTiptapEditor, runCommand, @@ -235,7 +225,7 @@ ok( "standalone Highlight button applies a default highlight to the selection" ); -fs.rmSync(out, { force: true }); +cleanupBundle(); fs.rmSync(book, { recursive: true, force: true }); console.log( diff --git a/tests/run-fullbook-test.mjs b/tests/run-fullbook-test.mjs index 205182d..3a92406 100644 --- a/tests/run-fullbook-test.mjs +++ b/tests/run-fullbook-test.mjs @@ -2,12 +2,10 @@ // renderer assembly + Tiptap modules, and verifies that a book assembled from // native chapters renders with correct chapter order, page-break anchors, // reorder behavior, title injection, and Copy-All native fidelity. -import esbuild from "esbuild"; import fs from "fs"; -import os from "os"; import path from "path"; -import url from "url"; import { JSDOM } from "jsdom"; +import { bundleForTest } from "./helpers/bundle.mjs"; const dom = new JSDOM("", { pretendToBeVisual: true, @@ -26,19 +24,10 @@ g.DOMParser = window.DOMParser; g.requestAnimationFrame = (cb) => setTimeout(cb, 0); g.cancelAnimationFrame = (id) => clearTimeout(id); -const out = path.join(os.tmpdir(), `folio-fullbook-${process.pid}.mjs`); -const entry = path.resolve("tests/.folio-fullbook-test-entry.ts"); -await esbuild.build({ - entryPoints: [entry], - bundle: true, - platform: "node", - format: "esm", - outfile: out, - write: true, - logLevel: "silent", +const { mod, cleanupBundle } = await bundleForTest({ + name: "folio-fullbook", + entryPoints: ["tests/.folio-fullbook-test-entry.ts"], }); - -const mod = await import(url.pathToFileURL(out).href); const { buildFullBookDoc, nativeNodeText, @@ -190,6 +179,6 @@ ok( "empty book assembles to empty doc" ); -fs.rmSync(out, { force: true }); +cleanupBundle(); console.log("\nFull Book tests: " + (failures === 0 ? "ALL PASSED" : failures + " FAILED")); process.exit(failures === 0 ? 0 : 1); diff --git a/tests/run-migration-test.mjs b/tests/run-migration-test.mjs index 10f4b71..bb458f8 100644 --- a/tests/run-migration-test.mjs +++ b/tests/run-migration-test.mjs @@ -8,37 +8,22 @@ // - idempotency (original .md untouched) // Builds src/main/migration.ts with esbuild (no Electron needed). // Run with: node tests/run-migration-test.mjs -import esbuild from "esbuild"; import fs from "fs"; import os from "os"; import path from "path"; -import url from "url"; +import { bundleForTest } from "./helpers/bundle.mjs"; -const out = path.join(os.tmpdir(), `folio-migration-${process.pid}.mjs`); -const entry = path.resolve(".folio-mig-test-entry.ts"); -fs.writeFileSync( - entry, - ` +const { mod, cleanupBundle } = await bundleForTest({ + name: "folio-migration", + entrySource: ` export { migrateChapterToNative, migrateBookToNative, isNativeChapter } from "./src/main/migration.ts"; export { createBook, loadBook, saveMeta } from "./src/main/project.ts"; export { setChapterContent, getChapterContent } from "./src/main/chapters.ts"; export { markdownToNative } from "./src/main/markdown-to-native.ts"; export { parseChapter, serializeChapter, migrateDoc, getChapterDoc, nativeChapterFile, CURRENT_DOC_SCHEMA_VERSION, isValidDoc } from "./src/main/native-doc.ts"; export { getRecoveryRoot, _resetSessionBackupGuard } from "./src/main/backup.ts"; -` -); -await esbuild.build({ - entryPoints: [entry], - bundle: true, - platform: "node", - format: "esm", - outfile: out, - write: true, - logLevel: "silent", +`, }); -fs.rmSync(entry, { force: true }); - -const mod = await import(url.pathToFileURL(out).href); const { createBook, setChapterContent, @@ -291,7 +276,7 @@ ok( // --------------------------------------------------------------------------- // Cleanup // --------------------------------------------------------------------------- -fs.rmSync(out, { force: true }); +cleanupBundle(); fs.rmSync(book, { recursive: true, force: true }); for (const d of backups) { fs.rmSync(path.join(getRecoveryRoot(), d), { recursive: true, force: true }); diff --git a/tests/run-office-export-test.mjs b/tests/run-office-export-test.mjs index 1669c71..d72bf99 100644 --- a/tests/run-office-export-test.mjs +++ b/tests/run-office-export-test.mjs @@ -4,27 +4,18 @@ // JSON, then inspects the produced ZIP contents to check that the manuscript, // its chapter order, and its formatting survive the native -> DOCX path. // Run with: node tests/run-office-export-test.mjs -import esbuild from "esbuild"; import fs from "fs"; import os from "os"; import path from "path"; -import url from "url"; import JSZip from "jszip"; +import { bundleForTest } from "./helpers/bundle.mjs"; -const out = path.resolve(`tests/.tmp-folio-office-${process.pid}.mjs`); -await esbuild.build({ - entryPoints: [path.resolve("src/main/office-export.ts")], - bundle: true, - platform: "node", - format: "esm", +const { mod, cleanupBundle } = await bundleForTest({ + name: "folio-office", + entryPoints: ["src/main/office-export.ts"], external: ["electron"], - outfile: out, - write: true, - logLevel: "silent", }); -const mod = await import(url.pathToFileURL(out).href); - let failures = 0; const check = (name, ok) => { console.log(`${ok ? "PASS" : "FAIL"} ${name}`); @@ -271,7 +262,7 @@ for (const [f, bytes] of origFiles) { check("folio.json unchanged", fs.readFileSync(path.join(bookDir, "folio.json")).equals(origManifest)); fs.rmSync(bookDir, { recursive: true, force: true }); -fs.rmSync(out, { force: true }); +cleanupBundle(); if (odtSkipped) { console.log("NOTE ODT verification requires LibreOffice; it was skipped."); diff --git a/tests/run-print-test.mjs b/tests/run-print-test.mjs index fca7100..d46d098 100644 --- a/tests/run-print-test.mjs +++ b/tests/run-print-test.mjs @@ -3,25 +3,14 @@ // renderer module and verifies that chapter breaks become print sections and // that every rich-text feature survives the native -> HTML conversion. // Run with: node tests/run-print-test.mjs -import esbuild from "esbuild"; import fs from "fs"; -import os from "os"; -import path from "path"; -import url from "url"; +import { bundleForTest } from "./helpers/bundle.mjs"; -const out = path.resolve(`tests/.tmp-folio-print-${process.pid}.mjs`); -await esbuild.build({ - entryPoints: [path.resolve("src/renderer/print-format.ts")], - bundle: true, - platform: "node", - format: "esm", +const { mod, cleanupBundle } = await bundleForTest({ + name: "folio-print", + entryPoints: ["src/renderer/print-format.ts"], external: ["electron"], - outfile: out, - write: true, - logLevel: "silent", }); - -const mod = await import(url.pathToFileURL(out).href); const renderPrintChaptersNative = mod.renderPrintChaptersNative; let failures = 0; @@ -136,7 +125,7 @@ check("renders external link", /Site<\/a>/.test( check("renders wikilink as internal anchor", /Go<\/a>/.test(html)); check("no raw wikilink brackets", !html.includes("[[Two")); -fs.rmSync(out, { force: true }); +cleanupBundle(); if (failures > 0) { console.error(`\n${failures} check(s) failed.`); diff --git a/tests/run-project-test.mjs b/tests/run-project-test.mjs index b528763..1ff5a34 100644 --- a/tests/run-project-test.mjs +++ b/tests/run-project-test.mjs @@ -1,24 +1,15 @@ // Standalone test for the pure project/recents logic. // Builds src/main/project.ts with esbuild (no Electron needed) and asserts behavior. // Run with: node tests/run-project-test.mjs -import esbuild from "esbuild"; import fs from "fs"; import os from "os"; import path from "path"; -import url from "url"; +import { bundleForTest } from "./helpers/bundle.mjs"; -const out = path.join(os.tmpdir(), `folio-project-${process.pid}.mjs`); -await esbuild.build({ - entryPoints: [path.resolve("src/main/project.ts")], - bundle: true, - platform: "node", - format: "esm", - outfile: out, - write: true, - logLevel: "silent", +const { mod, cleanupBundle } = await bundleForTest({ + name: "folio-project", + entryPoints: ["src/main/project.ts"], }); - -const mod = await import(url.pathToFileURL(out).href); const { APP_ID, META_FILE, @@ -78,7 +69,7 @@ const id = makeChapterId("Chapter One"); ok(id.startsWith("chapter-one-"), "chapter id slugified: " + id); ok(chapterFileName(id) === `${id}.md`, "filename is