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: "", src: '/file?x="', }); expect(html).toContain('data-wrnexus-runtime="viewer"'); expect(html).not.toContain(""); 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('
', 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 = '
'; 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);