Files
WRNexusJS/packages/viewer/test/runtime.test.ts
T

148 lines
5.2 KiB
TypeScript

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: "<script>bad</script>.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(['<html><script>alert(1)</script><img onerror="alert(1)"></html>']),
});
await settled();
expect(host.querySelector("script, img")).toBeNull();
const frame = host.querySelector("iframe")!;
expect(frame.getAttribute("sandbox")).toBe("");
expect(frame.srcdoc).not.toContain("<script>");
expect(frame.srcdoc).not.toContain("onerror");
[...host.querySelectorAll("button")].find((button) => button.textContent === "Source")!.click();
expect(host.textContent).toContain("<script>alert(1)</script>");
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<void>((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();
});