import { expect, test } from "bun:test"; import { Window } from "happy-dom"; import { mountViewer } from "../src/runtime.ts"; import { registerRenderer } from "../src/registry.ts"; async function settled() { for (let i = 0; i < 15; i++) await new Promise((resolve) => setTimeout(resolve, 5)); } function fixture() { const window = new Window(); const host = window.document.createElement("section"); window.document.body.append(host); return { window, host: host as unknown as HTMLElement }; } test("image controls, failure fallback, and teardown", async () => { const { window, host } = fixture(); const dispose = mountViewer(host, { name: ".png", blob: new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])]), }); await settled(); expect(host.querySelector("script")).toBeNull(); const image = host.querySelector("img")!; expect(image).not.toBeNull(); const zoom = [...host.querySelectorAll("button")].find( (button) => button.textContent === "Zoom in", )!; zoom.click(); expect(image.style.width).toBe("125%"); const url = image.src; image.dispatchEvent(new window.Event("error") as unknown as Event); expect(host.textContent).toContain("Image could not be displayed"); dispose(); expect(host.children.length).toBe(0); await expect(fetch(url)).rejects.toThrow(); await window.happyDOM.close(); }); test("unsafe links are omitted and unavailable files retain metadata", async () => { const { window, host } = fixture(); const dispose = mountViewer(host, { name: "file", src: "javascript:alert(1)", size: 123 }); await settled(); expect(host.querySelector("a")).toBeNull(); // The fallback card keeps the name and a human-readable size. expect(host.textContent).toContain("file"); expect(host.textContent).toContain("123 B"); dispose(); await window.happyDOM.close(); }); test("markup source never creates executable DOM", async () => { const { window, host } = fixture(); const dispose = mountViewer(host, { name: "hostile.html", blob: new Blob(['']), }); await settled(); expect(host.querySelector("script, img")).toBeNull(); const frame = host.querySelector("iframe")!; expect(frame.getAttribute("sandbox")).toBe(""); expect(frame.srcdoc).not.toContain(""); dispose(); await window.happyDOM.close(); }); test("disposing a pending custom renderer runs its eventual cleanup", async () => { const { window, host } = fixture(); let finish!: () => void; let cleaned = false; const ready = new Promise((resolve) => { finish = resolve; }); const unregister = registerRenderer({ kind: "text", exts: ["txt"], tier: 2, component: "Custom", mount: async () => { await ready; return () => { cleaned = true; }; }, }); const dispose = mountViewer(host, { name: "a.txt", blob: new Blob(["hello"]) }); await settled(); dispose(); finish(); await settled(); expect(cleaned).toBe(true); expect(host.children.length).toBe(0); unregister(); await window.happyDOM.close(); }); test("a renderer without a mount adapter never blocks the preview", async () => { const { window, host } = fixture(); const unregister = registerRenderer({ kind: "text", exts: ["txt"], tier: 2, component: "NoMount", }); const dispose = mountViewer(host, { name: "a.txt", blob: new Blob(["plain body"]) }); await settled(); expect(host.textContent).toContain("plain body"); dispose(); unregister(); await window.happyDOM.close(); }); test("an http source streams from its own URL instead of a blob copy", async () => { const { window, host } = fixture(); let bodyRequests = 0; const original = globalThis.fetch; globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => { const method = init?.method ?? "GET"; if (method === "HEAD") return new Response(null, { headers: { "content-type": "video/mp4" } }); bodyRequests++; return new Response("\0\0\0\0ftypisom"); }) as unknown as typeof fetch; try { const dispose = mountViewer(host, { name: "clip.mp4", src: "https://cdn.test/clip.mp4" }); await settled(); const video = host.querySelector("video")!; expect(video).not.toBeNull(); // The element points at the original URL: no blob copy, no full download. expect(video.getAttribute("src")).toBe("https://cdn.test/clip.mp4"); expect(bodyRequests).toBe(1); // the bounded sniff only dispose(); } finally { globalThis.fetch = original; } await window.happyDOM.close(); }); test("a local file still renders from an object URL under its sniffed type", async () => { const { window, host } = fixture(); const dispose = mountViewer(host, { name: "shot.png", blob: new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])]), }); await settled(); expect(host.querySelector("img")!.src.startsWith("blob:")).toBe(true); dispose(); await window.happyDOM.close(); });