import { afterEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { firstOf, sameOriginSource } from "../src/convert/sources.ts";
import { configureConversion } from "../src/convert/config.ts";
import { handleConvert } from "../src/convert/service.ts";
import { converterFormats, convertUrl, resetConverterProbe } from "../src/convert/client.ts";
import { loadOptional } from "../src/optional/load.ts";
import { sanitizeHtml } from "../src/parse/html.ts";
import { waveformPeaks } from "../src/renderers/waveform.ts";
import { mountViewer } from "../src/runtime.ts";
import { registerRenderer } from "../src/registry.ts";
import { extractText } from "../src/text.ts";
afterEach(() => {
configureConversion(undefined);
resetConverterProbe();
});
const tick = () => new Promise((resolve) => setTimeout(resolve, 40));
test("runtime preserves overridden chrome and mounts inside the renderer presentation", async () => {
const window = new Window();
const host = window.document.createElement("section");
window.document.body.append(host);
// Trusted test fixture representing SSR output from an overridden component.
host.innerHTML =
'';
const before = host.innerHTML;
const dispose = mountViewer(host as unknown as HTMLElement, {
name: "photo.png",
blob: new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])]),
});
await tick();
expect(host.querySelector(".custom-toolbar [data-viewer-title]")?.textContent).toBe("photo.png");
expect(
host.querySelector(".custom-content .custom-image [data-viewer-mount] img"),
).not.toBeNull();
dispose();
expect(host.innerHTML).toBe(before);
await window.happyDOM.close();
});
test("filmstrip consumes the thumbnail endpoint and releases its object URLs", async () => {
const window = new Window({ url: "https://thumb.test/demo" });
const host = window.document.createElement("section");
window.document.body.append(host);
const saved = globalThis.fetch;
const requests: string[] = [];
globalThis.fetch = (async (input, init) => {
const url = new URL(String(input));
requests.push(url.pathname);
if (url.pathname.endsWith("/capabilities"))
return Response.json({ formats: { ".psd": "image/png" } });
if (url.pathname.endsWith("/thumb"))
return new Response(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), {
headers: { "content-type": "image/png" },
});
if (init?.method === "HEAD") return new Response(null, { headers: { "content-length": "8" } });
return new Response(new Uint8Array([56, 66, 80, 83, 0, 0, 0, 0]));
}) as typeof fetch;
let dispose: (() => void) | undefined;
try {
dispose = mountViewer(host as unknown as HTMLElement, [
{ name: "art.psd", src: "https://thumb.test/art.psd" },
]);
await tick();
expect(requests).toContain("/api/viewer/thumb");
const image = host.querySelector(".wrn-viewer-file img") as unknown as HTMLImageElement;
expect(image).not.toBeNull();
const url = image.src;
dispose();
dispose = undefined;
await expect(saved(url)).rejects.toThrow();
} finally {
dispose?.();
globalThis.fetch = saved;
await window.happyDOM.close();
}
});
test("rotating links hit one stable cache entry, but denied access never reads it", async () => {
let allowed = true;
let conversions = 0;
let cancellations = 0;
configureConversion({
converters: [
{
name: "stable-test",
supports: () => true,
outputType: () => "application/pdf",
toPreview: async () => {
conversions++;
return { mime: "application/pdf", bytes: new TextEncoder().encode("%PDF") };
},
},
],
resolveSource: async () =>
allowed
? {
identity: "reports:key:revision1",
body: new ReadableStream({
cancel() {
cancellations++;
},
}),
}
: undefined,
});
const request = (token: string) =>
new Request(
`https://app.test/api/viewer/preview?name=a.docx&src=${encodeURIComponent("/api/viewer/file?token=" + token)}`,
);
expect((await handleConvert(request("first"), "preview")).headers.get("x-viewer-cache")).toBe(
"miss",
);
expect((await handleConvert(request("rotated"), "preview")).headers.get("x-viewer-cache")).toBe(
"hit",
);
expect(conversions).toBe(1);
expect(cancellations).toBeGreaterThanOrEqual(1);
allowed = false;
expect((await handleConvert(request("rotated"), "preview")).status).toBe(403);
});
test("source composition falls through on undefined and stops on errors", async () => {
const seen: string[] = [];
const result = await firstOf(
async () => {
seen.push("upload");
return undefined;
},
async () => {
seen.push("static");
return { body: new Response("static").body!, identity: "static" };
},
)(new Request("https://app.test"), { src: "/a", name: "a" });
expect(result?.identity).toBe("static");
expect(seen).toEqual(["upload", "static"]);
await expect(
firstOf(
async () => {
throw new Error("denied");
},
async () => {
throw new Error("must not run");
},
)(new Request("https://app.test"), { src: "/a", name: "a" }),
).rejects.toThrow("denied");
});
test("same-origin policy refuses viewer API links instead of bypassing private policy", async () => {
const source = sameOriginSource();
for (const src of [
"/api/viewer/file?token=bad",
"https://other.test/a",
"https://user:pass@app.test/a",
])
expect(await source(new Request("https://app.test"), { src, name: "a.docx" })).toBeUndefined();
});
test("client helpers use the supplied document origin without global document", async () => {
const fetched: string[] = [];
const request = (async (url: string) => {
fetched.push(String(url));
return Response.json({ formats: { ".docx": "application/pdf" } });
}) as unknown as typeof fetch;
expect(await converterFormats({ baseURI: "https://one.test/page", fetch: request })).toEqual({
".docx": "application/pdf",
});
expect(fetched).toEqual(["https://one.test/api/viewer/capabilities"]);
expect(convertUrl({ src: "/file", name: "a.docx" }, "text", "https://two.test/page")).toContain(
"https://two.test/api/viewer/text",
);
expect(
await loadOptional<{ ok: boolean }>("viewer-sheet", "https://three.test/page", async (url) => {
fetched.push(url);
return { ok: true };
}),
).toEqual({ ok: true });
expect(fetched[1]).toBe("https://three.test/__wrnexus/assets/viewer-sheet.js");
});
test("HTML sanitizer preserves text and layout while removing active and external content", async () => {
const window = new Window();
const html = sanitizeHtml(
'
Hello
link
safe
',
window as unknown as { DOMParser: new () => DOMParser },
);
expect(html).toContain("Hello
");
for (const bad of [
"script",
"onclick",
"iframe",
"javascript:",
"https://evil",
"onerror",
" {
expect(
waveformPeaks([new Float32Array([0, 0.5, 0, 0]), new Float32Array([0, 0, -2, 0])], 2),
).toEqual([0.5, 1]);
expect(waveformPeaks([new Float32Array(4)], 2)).toEqual([0, 0]);
});
test("filmstrip switching disposes each active renderer and supports keyboard selection", async () => {
const window = new Window();
const host = window.document.createElement("section");
window.document.body.append(host);
const clean: string[] = [];
const unregister = registerRenderer({
kind: "text",
exts: [".txt"],
tier: 2,
component: "TextTest",
mount: ({ parent, source }) => {
parent.textContent = source.name;
return () => {
clean.push(source.name);
};
},
});
const dispose = mountViewer(host as unknown as HTMLElement, [
{ name: "one.txt", blob: new Blob(["one"]) },
{ name: "two.txt", blob: new Blob(["two"]) },
]);
await tick();
const buttons = host.querySelectorAll(".wrn-viewer-file");
(buttons[1] as unknown as HTMLButtonElement).click();
await tick();
expect(clean).toEqual(["one.txt"]);
expect(buttons[1]!.getAttribute("aria-pressed")).toBe("true");
buttons[1]!.dispatchEvent(new window.KeyboardEvent("keydown", { key: "Home", bubbles: true }));
await tick();
expect(clean).toEqual(["one.txt", "two.txt"]);
expect(buttons[0]!.getAttribute("aria-pressed")).toBe("true");
dispose();
expect(clean).toEqual(["one.txt", "two.txt", "one.txt"]);
expect(host.children.length).toBe(0);
unregister();
await window.happyDOM.close();
});
test("text extraction rejects non-text and enforces bounded output", async () => {
const source = { src: "/a.docx", name: "a.docx" };
expect(
await extractText(source, {
baseURI: "https://app.test",
fetch: (async () =>
new Response("search words", {
headers: { "content-type": "text/plain" },
})) as unknown as typeof fetch,
}),
).toBe("search words");
await expect(
extractText(source, {
baseURI: "https://app.test",
fetch: (async () =>
new Response("%PDF", {
headers: { "content-type": "application/pdf" },
})) as unknown as typeof fetch,
}),
).rejects.toThrow("unavailable");
});