// @vitest-environment happy-dom import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildModuleUi } from "@ekklesia/module-ui-build"; import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk"; import { beforeAll, describe, expect, it } from "vitest"; import { loadModuleUi, MODULE_UI_ISOLATION_ATTRIBUTE, MODULE_UI_ISOLATION_ROOT_ATTRIBUTE, } from "../src/index.js"; /** What the artifact rendered, reached through the open shadow root the loader created (ADR 0040). */ function artifactText(container: Element): string { const shadowHost = container.querySelector(`[${MODULE_UI_ISOLATION_ATTRIBUTE}]`); return ( shadowHost?.shadowRoot?.querySelector(`[${MODULE_UI_ISOLATION_ROOT_ATTRIBUTE}]`)?.textContent ?? "" ); } const fixtureDirectory = path.join( path.dirname(fileURLToPath(import.meta.url)), "fixtures", "artifact-module", ); let artifactUrl: string; let artifactSource: string; /** An Ekklesia-shaped host: module routes live under a Core-owned prefix. */ function ekklesiaContext(requested: string[]): ModuleUiHostContext { return { moduleId: "example", api: { async request(pathname) { requested.push(`/api/v1/modules/example/${pathname.replace(/^\//u, "")}`); return new Response(null, { status: 200 }); }, }, principal: { kind: "authenticated", userId: "user-123" }, permissions: { has: () => true }, realtime: { subscribe: () => ({ close: () => undefined }) }, }; } /** A standalone host: one module, a different origin, a completely different URL shape, no Core. */ function standaloneContext(requested: string[]): ModuleUiHostContext { return { moduleId: "example", api: { async request(pathname) { requested.push(`https://example.test/api/${pathname.replace(/^\//u, "")}`); return new Response(null, { status: 200 }); }, }, principal: { kind: "anonymous" }, permissions: { has: () => false }, realtime: { subscribe: () => ({ close: () => undefined }) }, }; } /** * Proves the loader consumes exactly the browser ESM artifact `@ekklesia/module-ui-build` produces * (ADR 0023) — no second output format, no checked-in build output. The artifact is rebuilt from * source each run and served through a `data:` URL, a real module URL rather than a filesystem path. */ describe("loading a real compiled artifact", () => { beforeAll(async () => { const result = await buildModuleUi({ moduleDirectory: fixtureDirectory }); const primaryArtifact = result.artifacts.find((artifact) => artifact.kind === "ui"); if (!primaryArtifact) { throw new Error("The real-artifact fixture must declare a primary UI artifact."); } artifactSource = await readFile(primaryArtifact.outputFile, "utf8"); artifactUrl = `data:text/javascript;base64,${Buffer.from(artifactSource, "utf8").toString("base64")}`; }, 60_000); it("produces a self-contained artifact with no bare imports for a host to resolve", () => { expect(artifactSource).not.toMatch(/^import\s.*from\s*["'][^.]/mu); expect(artifactSource).toContain("export {"); expect(artifactSource).not.toContain("/api/v1/modules"); }); it("mounts the real artifact through the generic loader and unmounts it", async () => { const container = document.createElement("div"); const requested: string[] = []; const mounted = await loadModuleUi({ source: { moduleId: "example", version: "0.1.0", url: artifactUrl }, container, context: ekklesiaContext(requested), }); expect(artifactText(container)).toBe("example:200:user-123"); await mounted.unmount(); expect(container.children).toHaveLength(0); expect(mounted.mounted).toBe(false); }); it("compiles the fixture's stylesheet into the artifact and installs it in the isolated root", async () => { const container = document.createElement("div"); // No sidecar: the whole stylesheet travels inside the one declared JavaScript artifact. expect(artifactSource).toContain(".artifact-label"); const mounted = await loadModuleUi({ source: { moduleId: "example", version: "0.1.0", url: artifactUrl }, container, context: ekklesiaContext([]), }); const stylesheet = mounted.shadowRoot.querySelector("style")?.textContent ?? ""; expect(stylesheet).toContain(".artifact-label"); expect(stylesheet).toContain("var(--ekklesia-text-primary)"); expect(document.head.textContent ?? "").not.toContain(".artifact-label"); await mounted.unmount(); }); it("still rejects the real artifact when the host expects a different version", async () => { await expect( loadModuleUi({ source: { moduleId: "example", version: "0.2.0", url: artifactUrl }, container: document.createElement("div"), context: ekklesiaContext([]), }), ).rejects.toMatchObject({ code: "MODULE_UI_IDENTITY_MISMATCH" }); }); it("runs the same artifact under an Ekklesia host and a standalone host", async () => { const ekklesiaRequests: string[] = []; const standaloneRequests: string[] = []; const ekklesiaContainer = document.createElement("div"); const standaloneContainer = document.createElement("div"); const source = { moduleId: "example", version: "0.1.0", url: artifactUrl }; const inEkklesia = await loadModuleUi({ source, container: ekklesiaContainer, context: ekklesiaContext(ekklesiaRequests), }); const inStandalone = await loadModuleUi({ source, container: standaloneContainer, context: standaloneContext(standaloneRequests), }); // One artifact, one loader, two hosts. Every difference lives in the context the host supplied: // the module asked for "records" both times and resolved nothing itself. expect(ekklesiaRequests).toEqual(["/api/v1/modules/example/records"]); expect(standaloneRequests).toEqual(["https://example.test/api/records"]); expect(artifactText(ekklesiaContainer)).toBe("example:200:user-123"); expect(artifactText(standaloneContainer)).toBe("example:200:guest"); await inEkklesia.unmount(); await inStandalone.unmount(); }); });