Ship file viewer, editor improvements, and private package releases
This commit is contained in:
1 parent
645914955c
commit
a4a31032b9
158 files changed
+10523
-137
No files matched your search
@@ -0,0 +1,147 @@
|
||||
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();
|
||||
});
|
||||
Reference in new issue
Block a user