import { Blob as NodeBlob, File as NodeFile } from "node:buffer"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { closeBook, openEpub } from "../../src/epub/archive"; import { openPublication, publicationAdapterForFile, } from "../../src/epub/adapters"; import { readEntryText, readPublicationResourceBytes, } from "../../src/epub/entries"; import { exportMarkdown, exportReadingOrderHtml, patchPackageMetadata, rebuildEpub, } from "../../src/epub/export"; import { exportReadingNotes, importReadingNotes, type EpubReadingNotes, } from "../../src/epub/annotations"; import { resolvePackageHref, validateEntryPath } from "../../src/epub/paths"; import { applyReaderTheme, renderChapter, revokeRenderedChapter, } from "../../src/epub/reader"; import type { EpubBook } from "../../src/epub/types"; import { searchPublication } from "../../src/epub/search"; import { createEpubFixture } from "./fixture"; let opened: EpubBook[] = []; beforeEach(() => { vi.stubGlobal("Blob", NodeBlob); vi.stubGlobal("File", NodeFile); }); afterEach(async () => { for (const book of opened) await closeBook(book); opened = []; vi.unstubAllGlobals(); }); describe("EPUB path policy", () => { it("resolves package-relative references and rejects traversal", () => { expect( resolvePackageHref("EPUB/text/chapter.xhtml", "../images/cover.png#art"), ).toBe("EPUB/images/cover.png"); expect(() => resolvePackageHref("chapter.xhtml", "../outside")).toThrow( /escapes/u, ); expect(() => validateEntryPath("../escape")).toThrow(/traversal/u); }); }); describe("EPUB package inspection", () => { it("adapts bounded Markdown, HTML and plain text into explicit EPUB workspaces", async () => { expect(publicationAdapterForFile({ name: "notes.md", type: "" })).toBe( "markdown", ); const markdown = await openPublication( new File(["# Adapter title\n\nA **local** paragraph."], "notes.md", { type: "text/markdown", }), ); opened.push(markdown); expect(markdown.adapter).toMatchObject({ kind: "markdown", sourceFilename: "notes.md", }); expect(markdown.package.metadata.title).toBe("Adapter title"); expect(await readEntryText(markdown, "EPUB/content.xhtml")).toContain( "local", ); expect(markdown.issues).toContainEqual( expect.objectContaining({ code: "adapted-publication" }), ); const html = await openPublication( new File( [ "
first\nsecond", ); }); it("opens metadata, navigation, spine, and bounded inventory", async () => { const book = await openEpub(await createEpubFixture()); opened.push(book); expect(book.package.metadata).toMatchObject({ title: "Fixture Book", language: "en", identifier: "urn:test:book", }); expect(book.package.navigation[0]).toMatchObject({ label: "Opening chapter", path: "EPUB/chapter.xhtml", }); expect(book.package.spine[0]?.item?.path).toBe("EPUB/chapter.xhtml"); expect(book.summary).toMatchObject({ mimetypeFirst: true, mimetypeStored: true, encryptedResources: false, }); expect(book.issues.filter((item) => item.severity === "error")).toEqual([]); }); it("reports monotonic, named opening phases", async () => { const updates: Array<{ phase: string; progress: number }> = []; const book = await openEpub(await createEpubFixture(), (progress) => updates.push(progress), ); opened.push(book); expect(updates.map((update) => update.phase)).toEqual( expect.arrayContaining([ "archive", "entries", "container", "package", "navigation", "validation", "ready", ]), ); expect(updates.at(-1)).toMatchObject({ phase: "ready", progress: 0.96 }); for (const [index, update] of updates.entries()) { expect(update.progress).toBeGreaterThanOrEqual(0); expect(update.progress).toBeLessThanOrEqual(1); if (index > 0) expect(update.progress).toBeGreaterThanOrEqual( updates[index - 1]?.progress ?? 0, ); } }); it("reports active content and broken resources", async () => { const book = await openEpub( await createEpubFixture({ activeContent: true, brokenLink: true }), ); opened.push(book); expect(book.issues.map((item) => item.code)).toEqual( expect.arrayContaining(["active-content", "broken-link"]), ); }); it("parses direction, page progression, and fixed-layout metadata", async () => { const book = await openEpub( await createEpubFixture({ direction: "rtl", fixedLayout: true }), ); opened.push(book); expect(book.package).toMatchObject({ direction: "rtl", pageProgressionDirection: "rtl", rendition: { layout: "pre-paginated", orientation: "portrait", spread: "none", }, }); }); }); describe("safe rendering and editing", () => { it("removes active content, disables links, and resolves local images", async () => { vi.stubGlobal("URL", { ...URL, createObjectURL: vi.fn(() => "blob:fixture-cover"), revokeObjectURL: vi.fn(), }); const book = await openEpub( await createEpubFixture({ activeContent: true }), ); opened.push(book); const chapter = await renderChapter(book, "EPUB/chapter.xhtml"); expect(chapter.html).not.toMatch(/