Files
Ekklesia/surfaces/shell/tests/display-app.test.tsx
T
ekklesiaandClaude Opus 5 07835b7b56 feat: give module UI real stylesheets, and every artifact its own root
ADR 0023 refused stylesheet imports for a good reason: esbuild would emit a
`.css` sidecar beside the artifact, nothing would declare it, no host would
fetch it, and it would work in a local test and silently do nothing in a real
host. Inline style objects were the whole story. They cannot express
`:hover`, `:focus-visible`, `:disabled`, `@media`, or `@keyframes` — so an
accessible focus treatment was not expressible, and the shared elements added
in the previous commit are built from real CSS for exactly that reason.

Compile the stylesheet into the artifact instead of beside it. A module
imports plain `.css`; the canonical builder resolves local `@import`s through
a nested bundle and turns each file into its compiled text, which the author
declares as `defineModuleUi({ styles })`. One declared file still contains
everything: nothing extra to publish, nothing to fetch, and no way for CSS to
become an asset nobody serves. The build additionally fails if a CSS output
ever appears beside an artifact, so a future loader change cannot bring the
sidecar back. Two rules are enforced with their own codes: a module stylesheet
may reference `--ekklesia-*` and never declare one, and every `@import` must
resolve locally or the artifact would fetch styles at run time. Plain `.css`
only; other dialects are a toolchain decision needing their own ADR.

Then make the isolation real. The generic loader mounts every browser artifact
— primary, display, and later a contributed renderer — inside an open Shadow
DOM root it creates, installs the artifact's stylesheet there, and hands the
artifact an element inside it. Shadow DOM is the one mechanism a browser
actually enforces; a class prefix or a "scope your selectors" instruction is a
convention one mistake defeats, and the failure mode is a module silently
restyling the Dashboard. It applies unconditionally, including to artifacts
with no stylesheet, because a conditional boundary is one a module can fall
out of by editing a source file. Both wrappers are `display: contents` through
base rules installed before the artifact's own, so layout is unchanged and a
full-viewport display can still override `:host`. Unmount removes the whole
root in a `finally`, so a cleanup that throws cannot leave styles behind, and
remounting cannot accumulate a second copy.

This is a correctness boundary for styles and is not described as a sandbox:
the root is `open` precisely because module code is trusted, same-realm code
that could reach `document` anyway.

An artifact's DOM is no longer a direct child of the host's element, so the
mount ABI is versioned explicitly. `defineModuleUi` stamps
`MODULE_UI_ABI_VERSION` so it can never drift from the SDK the artifact
compiled against, and a version-1 artifact fails with its own code — "rebuild
your module", not a typo hunt. No dual acceptance, matching every manifest
cutover here. Migration is one `pnpm build`.

`pnpm boundaries` allows `.css` only under `src/ui/` and `src/display/`: a
stylesheet anywhere else in a module is the undeclared sidecar again, just
relocated.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 07:21:14 -05:00

753 lines
26 KiB
TypeScript

import { MODULE_UI_ABI_VERSION, type ModuleUiHostContext } from "@ekklesia/module-ui-sdk";
import { act, Component, type ReactElement, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { HostApp } from "../src/browser/HostApp.js";
import { DisplayApp } from "../src/display/DisplayApp.js";
import type {
EkklesiaHostConfig,
HostEventSourceFactory,
HostEventSourceOptions,
} from "../src/host-protocol.js";
import { deepQuery, isolatedMountTarget, isolatedText } from "./support/isolated-dom.js";
declare global {
// eslint-disable-next-line no-var
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
const apiBaseUrl = "https://ekklesia.example.test";
let container: HTMLDivElement;
let root: Root;
let consoleError: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
});
afterEach(async () => {
await act(async () => root.unmount());
container.remove();
consoleError.mockRestore();
});
function descriptorPayload(
id = "announcements",
version = "1.2.0",
viewer: unknown = {
kind: "authenticated",
userId: "user-1",
},
) {
return {
viewer,
module: {
api: { baseUrl: `/api/v1/modules/${id}` },
display: { url: `/api/v1/module-ui/${id}/${version}/display` },
id,
permissions: [`${id}.slideshow.view`],
realtime: { baseUrl: `/api/v1/module-realtime/${id}` },
version,
},
};
}
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
headers: { "content-type": "application/json" },
status,
});
}
interface PlatformOptions {
readonly descriptorStatus?: number;
readonly principal?: unknown;
}
function platform(options: PlatformOptions = {}) {
const calls: Array<{ readonly init: RequestInit | undefined; readonly url: string }> = [];
const fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
calls.push({ init, url });
if (url.endsWith("/api/v1/principal")) {
return json(options.principal ?? { kind: "authenticated", userId: "user-1" });
}
if (url.endsWith("/api/v1/module-displays/announcements")) {
return json(descriptorPayload(), options.descriptorStatus ?? 200);
}
if (url.endsWith("/api/v1/device-pairing/requests")) {
return json(
{ code: "482731", expiresAt: new Date().toISOString(), pollIntervalMs: 60_000 },
201,
);
}
if (url.endsWith("/api/v1/device-pairing/claims")) {
return json({ status: "pending" });
}
if (url.endsWith("/api/v1/modules/announcements/slides")) {
return json([{ id: "slide-1" }]);
}
return json({ code: "NOT_FOUND" }, 404);
}) as unknown as typeof globalThis.fetch;
return { calls, fetch };
}
class FakeEventSource {
readonly CLOSED = 2 as const;
readonly CONNECTING = 0 as const;
readonly OPEN = 1 as const;
readonly listeners = new Map<string, Set<EventListener>>();
closeCalls = 0;
readyState: number = this.CONNECTING;
addEventListener(type: string, listener: EventListener): void {
const listeners = this.listeners.get(type) ?? new Set<EventListener>();
listeners.add(listener);
this.listeners.set(type, listeners);
}
removeEventListener(type: string, listener: EventListener): void {
this.listeners.get(type)?.delete(listener);
}
close(): void {
this.closeCalls += 1;
this.readyState = this.CLOSED;
}
emit(type: string, data?: string): void {
if (type === "open") {
this.readyState = this.OPEN;
}
const event = Object.freeze({ data, type }) as unknown as Event;
for (const listener of this.listeners.get(type) ?? []) {
listener(event);
}
}
}
function eventSources() {
const calls: Array<{
readonly options: HostEventSourceOptions;
readonly source: FakeEventSource;
readonly url: string;
}> = [];
const factory = ((url: string, options: HostEventSourceOptions): EventSource => {
const source = new FakeEventSource();
calls.push({ options, source, url });
return source as unknown as EventSource;
}) as HostEventSourceFactory;
return { calls, factory };
}
function artifact(
id = "announcements",
version = "1.2.0",
options: {
readonly cleanup?: () => void;
readonly mount?: (container: Element, context: ModuleUiHostContext) => undefined | (() => void);
} = {},
) {
return {
default: {
abi: MODULE_UI_ABI_VERSION,
id,
mount(element: Element, context: ModuleUiHostContext) {
if (options.mount) {
return options.mount(element, context);
}
element.textContent = `${context.moduleId}:display`;
return () => {
options.cleanup?.();
element.replaceChildren();
};
},
version,
},
};
}
async function render(
config: EkklesiaHostConfig,
importModule?: (url: string) => Promise<unknown>,
moduleId = "announcements",
): Promise<void> {
await act(async () => {
root.render(
<DisplayApp
config={config}
{...(importModule ? { importModule } : {})}
moduleId={moduleId}
/>,
);
});
}
function query(testId: string): HTMLElement | null {
return container.querySelector(`[data-testid="${testId}"]`);
}
function moduleUrl(source: string): string {
return `data:text/javascript;base64,${Buffer.from(source, "utf8").toString("base64")}`;
}
describe("display host — full-viewport boundary", () => {
it("mounts only the display artifact with no Dashboard chrome", async () => {
const server = platform();
const imported: string[] = [];
await render({ apiBaseUrl, fetch: server.fetch }, async (url) => {
imported.push(url);
return artifact();
});
expect(imported).toEqual([`${apiBaseUrl}/api/v1/module-ui/announcements/1.2.0/display`]);
expect(isolatedText(query("display-module-container"))).toBe("announcements:display");
expect(container.querySelector("header")).toBeNull();
expect(container.querySelector("nav")).toBeNull();
expect(container.querySelector("aside")).toBeNull();
expect(container.textContent).not.toContain("Ekklesia");
expect(query("display-host")?.style.height).toBe("100%");
expect(query("display-host")?.style.minHeight).toBe("100dvh");
});
it("constructs the same browser-safe API, principal, permission, and realtime context", async () => {
const server = platform();
const live = eventSources();
let seenContext: ModuleUiHostContext | undefined;
await render({ apiBaseUrl, eventSourceFactory: live.factory, fetch: server.fetch }, async () =>
artifact("announcements", "1.2.0", {
mount(element, context) {
seenContext = context;
void context.api.request("slides");
const subscription = context.realtime.subscribe("state", {});
element.textContent = context.permissions.has("announcements.slideshow.view")
? "permitted"
: "denied";
return () => subscription.close();
},
}),
);
expect(Object.keys(seenContext ?? {}).sort()).toEqual([
"api",
"moduleId",
"permissions",
"principal",
"realtime",
]);
expect(seenContext?.principal).toEqual({ kind: "authenticated", userId: "user-1" });
expect(isolatedText(query("display-module-container"))).toBe("permitted");
expect(
server.calls.some((call) => call.url.endsWith("/api/v1/modules/announcements/slides")),
).toBe(true);
expect(live.calls[0]).toMatchObject({
options: { withCredentials: true },
url: `${apiBaseUrl}/api/v1/module-realtime/announcements/state`,
});
});
it("uses a genuine dynamic import and renders an Announcements snapshot with its direct object URL unchanged", async () => {
const server = platform();
const live = eventSources();
const requestedArtifacts: string[] = [];
const source = `
export default {
abi: ${MODULE_UI_ABI_VERSION},
id: "announcements",
version: "1.2.0",
mount(container, context) {
const image = document.createElement("img");
image.setAttribute("data-testid", "announcement-slide");
container.append(image);
const subscription = context.realtime.subscribe("state", {
onSnapshot(state) {
image.src = state.mode === "on" ? state.slides[state.currentSlide].url : "";
},
onUpdate(state) {
image.src = state.mode === "on" ? state.slides[state.currentSlide].url : "";
},
});
return () => { subscription.close(); container.replaceChildren(); };
},
};
`;
await render(
{ apiBaseUrl, eventSourceFactory: live.factory, fetch: server.fetch },
async (url) => {
requestedArtifacts.push(url);
return import(moduleUrl(source));
},
);
const deliveryUrl =
"https://objects.example.test/ekklesia-announcements-assets/slides%2Fwelcome.jpg?X-Amz-Signature=opaque";
await act(async () => {
live.calls[0]?.source.emit("open");
live.calls[0]?.source.emit(
"snapshot",
JSON.stringify({
payload: {
currentSlide: 0,
mode: "on",
slideSeconds: 7,
slides: [{ url: deliveryUrl }],
},
sequence: 1,
}),
);
});
expect(requestedArtifacts).toEqual([
`${apiBaseUrl}/api/v1/module-ui/announcements/1.2.0/display`,
]);
expect(deepQuery(container, '[data-testid="announcement-slide"]')?.getAttribute("src")).toBe(
deliveryUrl,
);
expect(container.innerHTML).not.toContain("socket.io");
expect(container.innerHTML).not.toContain("mqtt");
});
it("keeps one Announcements-shaped module's management and slideshow roots distinct", async () => {
const live = eventSources();
const requestedArtifacts: string[] = [];
const fetch = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/api/v1/principal")) {
return json({ kind: "authenticated", userId: "user-1" });
}
if (url.endsWith("/api/v1/dashboard/navigation")) {
return json({
groups: [],
ungrouped: [
{
module: {
api: { baseUrl: "/api/v1/modules/announcements" },
id: "announcements",
permissions: ["announcements.slideshow.view"],
realtime: { baseUrl: "/api/v1/module-realtime/announcements" },
ui: { url: "/api/v1/module-ui/announcements/1.2.0" },
version: "1.2.0",
},
name: "Announcements",
},
],
});
}
if (url.endsWith("/api/v1/module-displays/announcements")) {
return json(descriptorPayload());
}
return json({ code: "NOT_FOUND" }, 404);
}) as unknown as typeof globalThis.fetch;
const config = { apiBaseUrl, eventSourceFactory: live.factory, fetch };
const importer = async (url: string): Promise<unknown> => {
requestedArtifacts.push(url);
if (!url.endsWith("/display")) {
return artifact("announcements", "1.2.0", {
mount(element) {
element.textContent = "announcement management";
return () => element.replaceChildren();
},
});
}
return artifact("announcements", "1.2.0", {
mount(element, context) {
const image = document.createElement("img");
image.dataset.testid = "both-role-slide";
element.append(image);
const subscription = context.realtime.subscribe<{
readonly currentSlide: number;
readonly mode: "off" | "on";
readonly slides: readonly { readonly url: string }[];
}>("state", {
onSnapshot(state) {
image.src = state.mode === "on" ? (state.slides[state.currentSlide]?.url ?? "") : "";
},
});
return () => {
subscription.close();
element.replaceChildren();
};
},
});
};
await act(async () => root.render(<HostApp config={config} importModule={importer} />));
// The management artifact rendered inside its own isolated root, not the shell's own tree.
expect(isolatedText(container.querySelector('[data-testid="module-container"]'))).toContain(
"announcement management",
);
expect(container.querySelector("nav")).not.toBeNull();
expect(requestedArtifacts).toEqual([`${apiBaseUrl}/api/v1/module-ui/announcements/1.2.0`]);
await act(async () => root.unmount());
root = createRoot(container);
await act(async () =>
root.render(<DisplayApp config={config} importModule={importer} moduleId="announcements" />),
);
const deliveryUrl =
"https://objects.example.test/module-assets/slide.jpg?X-Amz-Signature=opaque";
await act(async () => {
live.calls[0]?.source.emit("open");
live.calls[0]?.source.emit(
"snapshot",
JSON.stringify({
payload: { currentSlide: 0, mode: "on", slides: [{ url: deliveryUrl }] },
sequence: 1,
}),
);
});
expect(requestedArtifacts).toEqual([
`${apiBaseUrl}/api/v1/module-ui/announcements/1.2.0`,
`${apiBaseUrl}/api/v1/module-ui/announcements/1.2.0/display`,
]);
expect(deepQuery(container, '[data-testid="both-role-slide"]')?.getAttribute("src")).toBe(
deliveryUrl,
);
expect(container.querySelector("nav")).toBeNull();
expect(container.querySelector("header")).toBeNull();
expect(JSON.stringify(requestedArtifacts)).not.toMatch(/socket\.io|mqtt|10\.|localhost/u);
});
});
describe("display host — authentication and controlled failures", () => {
it("shows a pairing code instead of a human login when it has no identity", async () => {
const server = platform({ descriptorStatus: 403 });
const importer = vi.fn(async () => artifact());
await render({ apiBaseUrl, fetch: server.fetch }, importer);
// An unattended screen must never be sent to an email/password form: nobody is standing at it.
expect(query("display-host-signed-out")).toBeNull();
expect(query("display-pairing")).not.toBeNull();
expect(query("display-pairing-code")?.textContent).toBe("482731");
// The protected artifact is still not fetched, which is the invariant that matters.
expect(importer).not.toHaveBeenCalled();
// And the display never asks the human principal endpoint at all.
expect(server.calls.some((call) => call.url.endsWith("/api/v1/principal"))).toBe(false);
});
it("puts no request id, secret, or permission on a screen anyone can read", async () => {
const server = platform({ descriptorStatus: 403 });
await render(
{ apiBaseUrl, fetch: server.fetch },
vi.fn(async () => artifact()),
);
const rendered = container.textContent ?? "";
expect(rendered).toContain("482731");
expect(rendered).not.toMatch(/announcements\.slideshow\.view|device-pairing|claim|token/u);
});
it("does not import protected display code when the display is unavailable", async () => {
const server = platform({ descriptorStatus: 404 });
const importer = vi.fn(async () => artifact());
await render({ apiBaseUrl, fetch: server.fetch }, importer);
// 404 is "there is no such display", which pairing cannot fix — so it stays a plain message.
expect(query("display-host-unavailable")).not.toBeNull();
expect(query("display-pairing")).toBeNull();
expect(importer).not.toHaveBeenCalled();
});
it("clears partial mount DOM, renders no caught detail, and retries in a fresh container", async () => {
const server = platform();
let attempts = 0;
const containers: Element[] = [];
const importer = vi.fn(async () => {
attempts += 1;
return artifact("announcements", "1.2.0", {
mount(element) {
containers.push(element);
if (attempts === 1) {
element.textContent = "secret at /srv/modules/announcements";
throw new Error("mount exploded with token=secret");
}
element.textContent = "recovered";
return () => element.replaceChildren();
},
});
});
await render({ apiBaseUrl, fetch: server.fetch }, importer);
expect(query("display-error")).not.toBeNull();
expect(container.textContent).not.toContain("/srv/");
expect(container.textContent).not.toContain("secret");
await act(async () => query("display-retry")?.click());
expect(query("display-error")).toBeNull();
expect(isolatedText(query("display-module-container"))).toBe("recovered");
expect(containers).toHaveLength(2);
expect(containers[0]).not.toBe(containers[1]);
expect(containers[0]?.isConnected).toBe(false);
expect(consoleError).toHaveBeenCalledWith(
"[ekklesia] display UI failure module=announcements code=MODULE_UI_MOUNT_FAILED",
);
});
it.each([
"error",
"unhandledrejection",
] as const)("turns a post-mount page %s into one sanitized failure with manual retry", async (eventType) => {
const server = platform();
const cleanups: Element[] = [];
const mountedInto: Element[] = [];
const importer = vi.fn(async () =>
artifact("announcements", "1.2.0", {
mount(element) {
mountedInto.push(element);
element.textContent = "private render at /srv/modules/announcements";
return () => {
cleanups.push(element);
element.replaceChildren();
};
},
}),
);
await render({ apiBaseUrl, fetch: server.fetch }, importer);
expect(isolatedText(query("display-module-container"))).toContain("/srv/modules");
consoleError.mockClear();
await act(async () => {
const event =
eventType === "error"
? new ErrorEvent("error", {
error: new Error("token=secret-render-error"),
message: "token=secret-render-error",
})
: new Event("unhandledrejection");
window.dispatchEvent(event);
// A second signal from the same failed render must not run cleanup or transition twice.
window.dispatchEvent(new Event(eventType));
});
expect(query("display-error")).not.toBeNull();
expect(isolatedText(query("display-module-container"))).toBe("");
expect(container.textContent).toBe("This display could not be loaded.Retry");
expect(container.textContent).not.toMatch(/secret|\/srv\//u);
expect(importer).toHaveBeenCalledTimes(1);
expect(cleanups).toEqual([mountedInto[0]]);
expect(consoleError).toHaveBeenCalledTimes(1);
expect(consoleError).toHaveBeenCalledWith(
`[ekklesia] display UI failure module=announcements code=${
eventType === "error" ? "DISPLAY_UI_RUNTIME_ERROR" : "DISPLAY_UI_UNHANDLED_REJECTION"
}`,
);
await act(async () => query("display-retry")?.click());
expect(importer).toHaveBeenCalledTimes(2);
expect(query("display-error")).toBeNull();
expect(isolatedText(query("display-module-container"))).toContain("/srv/modules");
expect(mountedInto[0]?.isConnected).toBe(false);
expect(mountedInto[1]?.isConnected).toBe(true);
});
it("catches a React render failure reported after the artifact mount has returned", async () => {
const server = platform();
const cleanup = vi.fn();
await render({ apiBaseUrl, fetch: server.fetch }, async () =>
artifact("announcements", "1.2.0", {
mount(element) {
const moduleRoot = createRoot(element);
class ReportRenderFailure extends Component<{ readonly children: ReactNode }> {
override componentDidCatch(error: unknown): void {
// React's browser default reports an uncaught render through the page's global error
// facility. This test boundary makes that report deterministic under happy-dom,
// whose test runner otherwise intercepts the global exception first.
window.dispatchEvent(
new ErrorEvent("error", {
cancelable: true,
error,
message: error instanceof Error ? error.message : "display render failure",
}),
);
}
override render(): ReactNode {
return this.props.children;
}
}
const BrokenDisplay = (): ReactElement => {
throw new Error("late component secret at /srv/modules/announcements");
};
moduleRoot.render(
<ReportRenderFailure>
<BrokenDisplay />
</ReportRenderFailure>,
);
return () => {
cleanup();
moduleRoot.unmount();
};
},
}),
);
await act(async () => undefined);
expect(query("display-error")).not.toBeNull();
expect(container.textContent).toBe("This display could not be loaded.Retry");
expect(container.textContent).not.toMatch(/secret|\/srv\//u);
expect(cleanup).toHaveBeenCalledTimes(1);
});
it("does not treat a module media/resource error as a crashed display runtime", async () => {
const server = platform();
const cleanup = vi.fn();
await render({ apiBaseUrl, fetch: server.fetch }, async () =>
artifact("announcements", "1.2.0", {
mount(element) {
const image = document.createElement("img");
image.dataset.testid = "display-media";
element.append(image);
return () => cleanup();
},
}),
);
await act(async () => {
deepQuery(container, '[data-testid="display-media"]')?.dispatchEvent(
new ErrorEvent("error", {
bubbles: true,
cancelable: true,
message: "a signed media URL expired",
}),
);
});
expect(query("display-error")).toBeNull();
expect(deepQuery(container, '[data-testid="display-media"]')).not.toBeNull();
expect(cleanup).not.toHaveBeenCalled();
});
it("removes the page failure listeners when a display attempt is torn down", async () => {
const server = platform();
const cleanup = vi.fn();
await render({ apiBaseUrl, fetch: server.fetch }, async () =>
artifact("announcements", "1.2.0", { cleanup }),
);
await act(async () => root.render(null));
expect(cleanup).toHaveBeenCalledTimes(1);
consoleError.mockClear();
window.dispatchEvent(new ErrorEvent("error", { message: "after teardown" }));
window.dispatchEvent(new Event("unhandledrejection"));
expect(cleanup).toHaveBeenCalledTimes(1);
expect(consoleError).not.toHaveBeenCalled();
});
});
describe("display host — independent instances and stale work", () => {
it("mounts and unmounts two browser instances independently", async () => {
const server = platform();
const cleanups: string[] = [];
const importer = async () =>
artifact("announcements", "1.2.0", {
cleanup: () => cleanups.push("cleanup"),
});
const config = { apiBaseUrl, fetch: server.fetch };
await act(async () => {
root.render(
<>
<DisplayApp
config={config}
importModule={importer}
key="first"
moduleId="announcements"
/>
<DisplayApp
config={config}
importModule={importer}
key="second"
moduleId="announcements"
/>
</>,
);
});
expect(container.querySelectorAll('[data-testid="display-module-container"]')).toHaveLength(2);
await act(async () => {
root.render(
<DisplayApp
config={config}
importModule={importer}
key="second"
moduleId="announcements"
/>,
);
});
expect(cleanups).toHaveLength(1);
expect(container.querySelectorAll('[data-testid="display-module-container"]')).toHaveLength(1);
expect(isolatedText(query("display-module-container"))).toBe("announcements:display");
});
it("discards a late import into its detached container without disturbing the newer route", async () => {
let releaseAnnouncements: (() => void) | undefined;
const pendingAnnouncements = new Promise<void>((resolve) => {
releaseAnnouncements = resolve;
});
const mountedInto: Element[] = [];
const staleUnmounts: Element[] = [];
const fetch = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/api/v1/principal")) {
return json({ kind: "authenticated", userId: "user-1" });
}
if (url.endsWith("/api/v1/module-displays/calendar")) {
return json(descriptorPayload("calendar", "2.0.0"));
}
return json(descriptorPayload());
}) as unknown as typeof globalThis.fetch;
const config = { apiBaseUrl, fetch };
const importer = async (url: string): Promise<unknown> => {
const isCalendar = url.includes("/calendar/");
if (!isCalendar) {
await pendingAnnouncements;
}
return artifact(isCalendar ? "calendar" : "announcements", isCalendar ? "2.0.0" : "1.2.0", {
mount(element) {
mountedInto.push(element);
element.textContent = isCalendar ? "calendar-live" : "announcements-stale";
return () => {
if (!isCalendar) {
staleUnmounts.push(element);
}
element.replaceChildren();
};
},
});
};
await render(config, importer);
await render(config, importer, "calendar");
expect(isolatedText(query("display-module-container"))).toBe("calendar-live");
const liveContainer = query("display-module-container");
await act(async () => {
releaseAnnouncements?.();
await pendingAnnouncements;
});
expect(query("display-module-container")).toBe(liveContainer);
expect(isolatedText(liveContainer)).toBe("calendar-live");
const liveMountTarget = isolatedMountTarget(liveContainer);
const staleContainer = mountedInto.find((element) => element !== liveMountTarget);
expect(staleContainer?.isConnected).toBe(false);
expect(staleUnmounts).toEqual([staleContainer]);
});
});