Files

123 lines
5.5 KiB
TypeScript

import { expect, test } from "bun:test";
import { readFileSync, readdirSync } from "node:fs";
import { createRequire } from "node:module";
import { renderComponent } from "../../test/src/index.ts";
import { viewerPlugin } from "../src/plugin.ts";
import { compileWrnFile } from "../../compiler/src/index.ts";
import {
mergePluginAssets,
readPluginAsset,
runtimeScriptsForMarkup,
} from "../../dev-server/src/plugin-assets.ts";
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
for (const filename of readdirSync(new URL("../components", import.meta.url))) {
test(`${filename} renders its presentation contract`, async () => {
const source = readFileSync(new URL(`../components/${filename}`, import.meta.url), "utf8");
const html = await renderComponent(source, { class: "custom-presentation" });
expect(html).toContain("custom-presentation");
expect(html).toContain(filename === "Viewer.wrn" ? "data-wrnexus-runtime" : "data-viewer-");
});
}
test("Viewer renders escaped source attributes and runtime marker", async () => {
const source = readFileSync(new URL("../components/Viewer.wrn", import.meta.url), "utf8");
const html = await renderComponent(source, {
name: "<script>alert(1)</script>",
src: '/file?x="',
});
expect(html).toContain('data-wrnexus-runtime="viewer"');
expect(html).not.toContain("<script>alert(1)</script>");
expect(html).toContain('data-component="ViewerShell"');
});
test("framework bundles editor separately and injects only the viewer runtime", async () => {
const runtimes = viewerPlugin().clientRuntimes as ClientRuntimeDefinition[];
const assets = mergePluginAssets(runtimes, []);
const viewer = assets.find((asset) => asset.id === "runtime:viewer")!;
const editor = assets.find((asset) => asset.id === "runtime:viewer-code")!;
const decode = (body: string | ArrayBuffer) =>
typeof body === "string" ? body : new TextDecoder().decode(body);
const main = decode(await readPluginAsset(viewer));
expect(main).not.toContain("class EditorView");
expect(main).toContain("/__wrnexus/assets/viewer-code.js");
expect(decode(await readPluginAsset(editor))).toContain("mountBuilderSourceEditor");
expect(
runtimeScriptsForMarkup('<section data-wrnexus-runtime="viewer"></section>', runtimes),
).toHaveLength(1);
});
test("example page compiles with explicit Viewer import", () => {
const path = new URL("../../../examples/basic-app/app/pages/file-viewer.wrn", import.meta.url);
expect(compileWrnFile(readFileSync(path, "utf8"), path.pathname)).toContain("Viewer");
});
test("plugin contributes existing browser assets and mounts no conversion routes", () => {
const plugin = viewerPlugin();
expect(plugin.routeEntries).toBeUndefined();
expect(Array.isArray(plugin.componentDirs)).toBe(true);
for (const file of [
new URL("../src/runtime.ts", import.meta.url),
new URL("../viewer.css", import.meta.url),
])
expect(readFileSync(file, "utf8").length).toBeGreaterThan(0);
// The two core runtimes, plus one per installed optional parser (and the
// pdf.js worker). Only the core viewer runtime is ever injected into a page.
const ids = (plugin.clientRuntimes as ClientRuntimeDefinition[]).map((runtime) => runtime.id);
expect(ids.slice(0, 2)).toEqual(["viewer-code", "viewer"]);
for (const id of ids.slice(2)) expect(id.startsWith("viewer-")).toBe(true);
expect(plugin.styleSources).toHaveLength(1);
});
test("optional parsers are contributed only when their package resolves", () => {
const runtimes = viewerPlugin().clientRuntimes as ClientRuntimeDefinition[];
const ids = new Set(runtimes.map((runtime) => runtime.id));
const resolvable = (specifier: string) => {
try {
createRequire(new URL("../src/plugin.ts", import.meta.url)).resolve(specifier);
return true;
} catch {
return false;
}
};
for (const [id, specifier] of [
["viewer-sheet", "xlsx"],
["viewer-docx", "mammoth"],
["viewer-zip", "fflate"],
["viewer-pdfjs", "pdfjs-dist"],
] as const)
expect(ids.has(id)).toBe(resolvable(specifier));
// pdf.js is useless without its worker asset.
expect(ids.has("viewer-pdfjs-worker")).toBe(ids.has("viewer-pdfjs"));
});
test("optional runtimes are served but never injected into a page", () => {
const runtimes = viewerPlugin().clientRuntimes as ClientRuntimeDefinition[];
const markup = '<section data-wrnexus-runtime="viewer"></section>';
expect(runtimeScriptsForMarkup(markup, runtimes)).toHaveLength(1);
const assets = mergePluginAssets(runtimes, []);
for (const id of ["viewer-sheet", "viewer-zip", "viewer-docx", "viewer-pdfjs"]) {
if (!runtimes.some((runtime) => runtime.id === id)) continue;
const asset = assets.find((item) => item.id === `runtime:${id}`)!;
expect(asset.publicPath).toBe(`/__wrnexus/assets/${id}.js`);
}
});
test("each optional runtime bundles for the browser and exports its parser", async () => {
const runtimes = viewerPlugin().clientRuntimes as ClientRuntimeDefinition[];
const assets = mergePluginAssets(runtimes, []);
const decode = (body: string | ArrayBuffer) =>
typeof body === "string" ? body : new TextDecoder().decode(body);
for (const [id, exported] of [
["viewer-sheet", "readWorkbook"],
["viewer-docx", "docxToHtml"],
["viewer-zip", "listZip"],
["viewer-pdfjs", "renderPdf"],
] as const) {
const asset = assets.find((item) => item.id === `runtime:${id}`);
if (!asset) continue;
const code = decode(await readPluginAsset(asset));
expect(code.length).toBeGreaterThan(0);
expect(code).toContain(exported);
}
}, 120_000);