158 lines
5.7 KiB
TypeScript
158 lines
5.7 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('hideName hides only the visual title and preserves the file identity',async()=>{
|
|
const {window,host}=fixture();
|
|
const dispose=mountViewer(host,{name:'report.txt',blob:new Blob(['hello'])},{hideName:true});
|
|
await settled();
|
|
expect(host.querySelector<HTMLElement>('[data-viewer-title]')?.hidden??host.querySelector<HTMLElement>('.wrn-viewer-toolbar span')?.hidden).toBe(true);
|
|
expect(host.getAttribute('aria-label')).toBe('report.txt');
|
|
expect(host.querySelector<HTMLAnchorElement>('a[download]')?.download).toBe('report.txt');
|
|
dispose();window.close();
|
|
});
|
|
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();
|
|
});
|