Files

165 lines
6.3 KiB
TypeScript

// @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, NO_MODULE_UI_CONTRIBUTIONS } 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 {
contributions: NO_MODULE_UI_CONTRIBUTIONS,
display: undefined,
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 {
contributions: NO_MODULE_UI_CONTRIBUTIONS,
display: { url: "https://example.test/display" },
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();
});
});