import { describe, expect, test } from "bun:test"; import { Window } from "happy-dom"; import { openViewer, sourceFromTrigger, startViewerTriggers } from "../src/overlay.ts"; import { mountViewer, startViewerRuntime } from "../src/runtime.ts"; async function settled(ticks = 30) { for (let i = 0; i < ticks; i++) await new Promise((done) => setTimeout(done, 5)); } function page() { const window = new Window({ url: "https://app.test/" }); return { window, doc: window.document as unknown as Document }; } const csv = { name: "data.csv", blob: new Blob(["a,b\n1,2\n"]) }; describe("opening a preview over the page", () => { test("the overlay mounts a viewer and offers a way out", async () => { const { window, doc } = page(); const opened = openViewer(csv, { document: doc }); await settled(); const dialog = doc.querySelector(".wrn-viewer-dialog")!; expect(dialog).not.toBeNull(); expect(dialog.querySelector(".wrn-viewer-content")!.getAttribute("data-viewer-state")).toBe( "ready", ); expect(dialog.querySelector('[data-viewer-action="close"]')).not.toBeNull(); opened.close(); expect(doc.querySelector(".wrn-viewer-dialog")).toBeNull(); await window.happyDOM.close(); }); test("closing disposes the viewer rather than leaving it behind the overlay", async () => { const { window, doc } = page(); const opened = openViewer(csv, { document: doc }); await settled(); const host = doc.querySelector(".wrn-viewer-dialog .wrn-viewer")!; expect(host.querySelector("table")).not.toBeNull(); opened.close(); // The host went with the dialog, so nothing keeps running off-screen. expect(host.isConnected).toBe(false); await window.happyDOM.close(); }); test("closing twice, or from the dialog's own close event, is safe", async () => { const { window, doc } = page(); let closes = 0; const opened = openViewer(csv, { document: doc, onClose: () => closes++ }); await settled(); doc.querySelector(".wrn-viewer-dialog")!.dispatchEvent(new window.Event("close") as never); opened.close(); expect(closes).toBe(1); await window.happyDOM.close(); }); test("a click on the dialog itself is a backdrop click", async () => { const { window, doc } = page(); openViewer(csv, { document: doc }); await settled(); const dialog = doc.querySelector(".wrn-viewer-dialog")!; // A click inside the viewer must not close it. dialog .querySelector(".wrn-viewer")! .dispatchEvent(new window.MouseEvent("click", { bubbles: true }) as never); expect(doc.querySelector(".wrn-viewer-dialog")).not.toBeNull(); dialog.dispatchEvent(new window.MouseEvent("click", { bubbles: true }) as never); expect(doc.querySelector(".wrn-viewer-dialog")).toBeNull(); await window.happyDOM.close(); }); }); describe("triggers", () => { test("a data attribute is read into a source", () => { const { window, doc } = page(); const button = doc.createElement("button"); button.dataset.viewerOpen = ""; button.dataset.src = "/files/a.pdf"; button.dataset.name = "a.pdf"; button.dataset.size = "120"; expect(sourceFromTrigger(button)).toEqual({ src: "/files/a.pdf", key: undefined, store: undefined, name: "a.pdf", mime: undefined, size: 120, }); // Nothing to open is not an error; the click is simply ignored. expect(sourceFromTrigger(doc.createElement("button"))).toBeUndefined(); void window.happyDOM.close(); }); test("clicking an element with data-viewer-open opens the preview", async () => { const { window, doc } = page(); const stop = startViewerTriggers(doc); const button = doc.createElement("button"); button.dataset.viewerOpen = ""; button.dataset.src = "https://app.test/notes.csv"; button.dataset.name = "notes.csv"; doc.body.append(button); const original = globalThis.fetch; globalThis.fetch = (async () => new Response("a,b\n1,2\n", { headers: { "content-type": "text/csv" } })) as never; try { button.dispatchEvent(new window.MouseEvent("click", { bubbles: true }) as never); await settled(); expect(doc.querySelector(".wrn-viewer-dialog")).not.toBeNull(); expect(doc.querySelector(".wrn-viewer-dialog table")).not.toBeNull(); } finally { globalThis.fetch = original; stop(); } await window.happyDOM.close(); }); }); describe("layout modes", () => { test("fullScreen mounts covering the page, and Close returns it inline", async () => { const { window, doc } = page(); const host = doc.createElement("section"); doc.body.append(host); const dispose = mountViewer(host as never, csv, { fullScreen: true }); await settled(); expect(host.classList.contains("wrn-viewer-expanded")).toBe(true); const close = host.querySelector('[data-viewer-action="close"]') as HTMLButtonElement; expect(close).not.toBeNull(); close.click(); expect(host.classList.contains("wrn-viewer-expanded")).toBe(false); // The control goes with the state it exits. expect(host.querySelector('[data-viewer-action="close"]')).toBeNull(); dispose(); await window.happyDOM.close(); }); test("an inline viewer offers no Close, because there is nothing to close", async () => { const { window, doc } = page(); const host = doc.createElement("section"); doc.body.append(host); const dispose = mountViewer(host as never, csv); await settled(); expect(host.querySelector('[data-viewer-action="close"]')).toBeNull(); dispose(); await window.happyDOM.close(); }); test("layout=page drops the card chrome", async () => { const { window, doc } = page(); const host = doc.createElement("section"); doc.body.append(host); const dispose = mountViewer(host as never, csv, { layout: "page" }); await settled(); expect(host.classList.contains("wrn-viewer-as-page")).toBe(true); dispose(); await window.happyDOM.close(); }); test("the runtime re-mounts when the layout attributes change", async () => { const { window, doc } = page(); const host = doc.createElement("section"); host.dataset.wrnexusRuntime = "viewer"; host.dataset.name = "data.csv"; host.dataset.src = "https://app.test/data.csv"; host.dataset.fullScreen = "false"; doc.body.append(host); const original = globalThis.fetch; globalThis.fetch = (async () => new Response("a,b\n1,2\n", { headers: { "content-type": "text/csv" } })) as never; const stop = startViewerRuntime(doc); try { await settled(); expect(host.classList.contains("wrn-viewer-expanded")).toBe(false); // Without this in the remount signature the attribute change is ignored. host.dataset.fullScreen = "true"; await settled(); expect(host.classList.contains("wrn-viewer-expanded")).toBe(true); } finally { globalThis.fetch = original; stop(); } await window.happyDOM.close(); }); });