feat(shell): collapsible sidebar, account menu, and phone drawer
CI / verify (pull_request) Failing after 57s
CI / infrastructure-smoke (pull_request) Skipped
CI / migration-smoke (pull_request) Skipped
CI / package-release (pull_request) Skipped
CI / package-publication (pull_request) Skipped
CI / image-build (pull_request) Skipped
CI / deploy-smoke (same-origin) (pull_request) Skipped
CI / deploy-smoke (split) (pull_request) Skipped

The Dashboard's shell now follows DESIGN.md §4 (ADR 0058):

- A collapsible left sidebar replaces the header-and-column layout. It
  is icons and labels when expanded and icons only when collapsed; each
  collapsed item keeps its name, visually hidden, and shows a tooltip
  on hover and on keyboard focus (WCAG 2.2 §1.4.13). The Overview comes
  first, then the server's groups and modules. There is no top bar.
- The expanded/collapsed choice is remembered in this browser's
  localStorage, behind a SidebarPreference interface that a synced
  store can replace later.
- An account menu at the bottom of the sidebar follows the WAI-ARIA menu
  button pattern and offers only Sign out, which now lives there rather
  than in the header.
- On phones (below 48em) the same navigation opens in a modal drawer:
  the page behind it is inert and focus is trapped inside it. Escape,
  its close button, and the backdrop close it and return focus to the
  menu button. Choosing a page closes it and focuses the page. Changing
  layout keeps the route, the desktop mode, and the mounted module.
- Lucide (lucide-react 1.48.0, shell only) is Ekklesia's icon
  vocabulary. Every module shows one generic icon until manifest v9
  lets modules declare their own; there is no per-module icon map.
- The identity slot is a neutral placeholder with no inputs, waiting
  for branding (#101).

The tooltip, menu, and drawer stay shell-local until a second consumer
needs them (ADR 0039). The deploy smoke now signs out through the
account menu, and drives the drawer at phone size in a real browser.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-14 17:49:56 -05:00
co-authored by Claude Opus 5.5
parent f3b7e19651
commit af31724cb4
24 changed files with 2559 additions and 189 deletions
@@ -0,0 +1,687 @@
import { MODULE_UI_ABI_VERSION, type ModuleUiHostContext } from "@ekklesia/module-ui-sdk";
import { act } 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 { createMemoryHostHistory, type MemoryHostHistory } from "../src/browser/host-history.js";
import { createMemoryHostViewport, type MemoryHostViewport } from "../src/browser/host-viewport.js";
import {
createLocalSidebarPreference,
createMemorySidebarPreference,
type SidebarPreference,
} from "../src/browser/sidebar-preference.js";
import type { EkklesiaHostConfig } from "../src/host-protocol.js";
import { isolatedText } from "./support/isolated-dom.js";
declare global {
// eslint-disable-next-line no-var
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
/**
* The Dashboard shell's navigation (ADR 0058): the collapsible desktop sidebar, its account area,
* and the phone drawer. Every test drives the real `HostApp` with in-memory history, viewport, and
* sidebar preference, so routing, resizing, and persistence are all observable.
*/
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();
window.localStorage.clear();
});
function entry(id: string, name: string, version = "0.1.0") {
return {
module: {
api: { baseUrl: `/api/v1/modules/${id}` },
contributions: [],
display: null,
id,
permissions: [],
realtime: { baseUrl: `/api/v1/module-realtime/${id}` },
ui: { url: `/api/v1/module-ui/${id}/${version}` },
version,
},
name,
};
}
// What the server's navigation returns: the eligible modules only. A disabled module (`retired`) is
// simply absent from it (ADR 0028), which is the whole of "hidden from navigation".
const NAVIGATION = {
groups: [{ id: "media", label: "Media", modules: [entry("signage", "Signage")], order: 10 }],
ungrouped: [entry("calendar", "Calendar", "2.0.0")],
};
interface Platform {
readonly fetch: typeof fetch;
readonly calls: string[];
}
function platform(): Platform {
let authenticated = true;
const calls: string[] = [];
const implementation = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input);
calls.push(url);
if (url.endsWith("/api/auth/sign-out")) {
authenticated = false;
return Response.json({ success: true });
}
if (url.endsWith("/api/v1/principal")) {
return Response.json(
authenticated ? { kind: "authenticated", userId: "user-1" } : { kind: "anonymous" },
);
}
if (url.endsWith("/api/v1/dashboard/navigation")) {
return authenticated
? Response.json(NAVIGATION)
: Response.json({ code: "AUTHORIZATION_DENIED" }, { status: 403 });
}
if (url.endsWith("/api/v1/devices")) {
return Response.json({ code: "AUTHORIZATION_DENIED" }, { status: 403 });
}
return Response.json({ code: "NOT_FOUND" }, { status: 404 });
}) as unknown as typeof fetch;
return { calls, fetch: implementation };
}
function artifactFor(url: string) {
const id = url.includes("/calendar/") ? "calendar" : "signage";
return {
default: {
abi: MODULE_UI_ABI_VERSION,
id,
mount(element: Element, context: ModuleUiHostContext) {
element.textContent = `${context.moduleId} mounted`;
return () => {
element.textContent = "";
};
},
version: id === "calendar" ? "2.0.0" : "0.1.0",
},
};
}
interface Rendered {
readonly history: MemoryHostHistory;
readonly viewport: MemoryHostViewport;
readonly preference: SidebarPreference;
readonly platform: Platform;
}
async function render(
options: {
readonly path?: string;
readonly compact?: boolean;
readonly preference?: SidebarPreference;
readonly history?: MemoryHostHistory;
readonly viewport?: MemoryHostViewport;
} = {},
): Promise<Rendered> {
const history = options.history ?? createMemoryHostHistory(options.path ?? "/");
const viewport = options.viewport ?? createMemoryHostViewport(options.compact ?? false);
const preference = options.preference ?? createMemorySidebarPreference();
const stub = platform();
const config: EkklesiaHostConfig = { apiBaseUrl, fetch: stub.fetch };
await act(async () => {
root.render(
<HostApp
config={config}
history={history}
importModule={async (url) => artifactFor(url)}
sidebarPreference={preference}
viewport={viewport}
/>,
);
});
return { history, platform: stub, preference, viewport };
}
async function remount(rendered: Rendered, preference = rendered.preference): Promise<void> {
await act(async () => {
root.unmount();
});
root = createRoot(container);
await render({ history: rendered.history, preference, viewport: rendered.viewport });
}
function query(testId: string): HTMLElement | null {
return container.querySelector<HTMLElement>(`[data-testid="${testId}"]`);
}
function required(testId: string): HTMLElement {
const element = query(testId);
if (!element) {
throw new Error(`missing ${testId}`);
}
return element;
}
async function click(element: Element, init: MouseEventInit = {}): Promise<MouseEvent> {
const event = new MouseEvent("click", { bubbles: true, button: 0, cancelable: true, ...init });
await act(async () => {
element.dispatchEvent(event);
});
return event;
}
async function key(element: Element, keyName: string, init: KeyboardEventInit = {}) {
const event = new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: keyName,
...init,
});
await act(async () => {
element.dispatchEvent(event);
});
return event;
}
async function focus(element: HTMLElement): Promise<void> {
await act(async () => {
element.focus();
});
}
const sidebar = (): HTMLElement => required("host-sidebar");
const navLink = (id: string): HTMLElement => required(`select-${id}`);
describe("desktop sidebar", () => {
it("is expanded by default: icons and labels, Overview first, then the modules in the server's order", async () => {
await render();
expect(sidebar().dataset.sidebar).toBe("expanded");
const links = [...sidebar().querySelectorAll("nav a")];
expect(links.map((link) => link.textContent)).toEqual(["Overview", "Signage", "Calendar"]);
for (const link of links) {
const icon = link.querySelector("svg");
expect(icon?.getAttribute("aria-hidden"), link.textContent ?? "").toBe("true");
}
expect(query("nav-tooltip")).toBeNull();
});
it("draws the Overview with its own icon and every module with the one generic module icon", async () => {
await render();
expect(navLink("overview").querySelector("svg")?.dataset.icon).toBe("overview");
for (const id of ["signage", "calendar"]) {
expect(navLink(id).querySelector("svg")?.dataset.icon, id).toBe("module");
expect(navLink(id).querySelector("svg")?.classList.contains("lucide-blocks"), id).toBe(true);
}
});
it("marks the open page, the Overview or a module, with aria-current", async () => {
await render({ path: "/calendar" });
expect(navLink("calendar").getAttribute("aria-current")).toBe("page");
expect(navLink("overview").getAttribute("aria-current")).toBeNull();
await click(navLink("overview"));
expect(navLink("overview").getAttribute("aria-current")).toBe("page");
expect(navLink("calendar").getAttribute("aria-current")).toBeNull();
});
it("does not list a disabled module, whose address still says it is unavailable", async () => {
await render({ path: "/retired" });
expect(query("select-retired")).toBeNull();
expect(query("host-module-unavailable")).not.toBeNull();
expect(container.querySelector('nav [aria-current="page"]')).toBeNull();
});
it("collapses to icons only, through a labelled toggle that says what it controls", async () => {
const { preference } = await render();
const toggle = required("sidebar-toggle");
const nav = sidebar().querySelector("nav");
expect(toggle.tagName).toBe("BUTTON");
expect(toggle.getAttribute("aria-label")).toBe("Collapse navigation");
expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(toggle.getAttribute("aria-controls")).toBe(nav?.id);
expect(nav?.id).toBeTruthy();
await click(toggle);
expect(sidebar().dataset.sidebar).toBe("collapsed");
expect(toggle.getAttribute("aria-label")).toBe("Expand navigation");
expect(toggle.getAttribute("aria-expanded")).toBe("false");
expect(preference.read()).toBe("collapsed");
// Icon-only to the eye, still named to assistive technology.
for (const id of ["overview", "signage", "calendar"]) {
const label = navLink(id).querySelector("[data-nav-label]");
expect(label?.classList.contains("ek-shell-visually-hidden"), id).toBe(true);
expect(navLink(id).textContent, id).not.toBe("");
}
await click(toggle);
expect(sidebar().dataset.sidebar).toBe("expanded");
expect(preference.read()).toBe("expanded");
});
it("keeps the chosen mode across a remount, as across a reload", async () => {
const rendered = await render();
await click(required("sidebar-toggle"));
await remount(rendered);
expect(sidebar().dataset.sidebar).toBe("collapsed");
});
it("keeps it in this browser's own storage, so a reload with a fresh page still finds it", async () => {
const rendered = await render({
preference: createLocalSidebarPreference(window.localStorage),
});
await click(required("sidebar-toggle"));
await remount(rendered, createLocalSidebarPreference(window.localStorage));
expect(sidebar().dataset.sidebar).toBe("collapsed");
});
it("names a collapsed item in a tooltip on keyboard focus and on hover, and Escape dismisses it", async () => {
await render({ preference: createMemorySidebarPreference("collapsed") });
await focus(navLink("signage"));
const tooltip = required("nav-tooltip");
expect(tooltip.textContent).toBe("Signage");
// A visual duplicate of the link's own accessible name, so it is not announced twice.
expect(tooltip.getAttribute("aria-hidden")).toBe("true");
await key(navLink("signage"), "Escape");
expect(query("nav-tooltip")).toBeNull();
// Escape hides the tooltip without moving focus.
expect(document.activeElement).toBe(navLink("signage"));
await act(async () => {
navLink("signage").blur();
});
await act(async () => {
navLink("calendar").dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
});
expect(required("nav-tooltip").textContent).toBe("Calendar");
});
it("shows no tooltip while expanded, where the label is already visible", async () => {
await render();
await focus(navLink("signage"));
expect(query("nav-tooltip")).toBeNull();
});
it("names the collapse toggle and the account control in tooltips when collapsed", async () => {
await render({ preference: createMemorySidebarPreference("collapsed") });
await focus(required("sidebar-toggle"));
expect(required("nav-tooltip").textContent).toBe("Expand navigation");
await focus(required("host-account-menu"));
expect(required("nav-tooltip").textContent).toBe("Your account");
});
it("is the only navigation: no application top bar", async () => {
await render();
expect(container.querySelector('[data-testid="host-shell"] > header')).toBeNull();
expect(query("mobile-navigation-trigger")).toBeNull();
// The neutral identity placeholder sits at the top of the sidebar, and is not a heading: each
// page's own title is.
const brand = required("shell-brand");
expect(sidebar().contains(brand)).toBe(true);
expect(brand.querySelector("h1, h2")).toBeNull();
});
it("orders keyboard focus top to bottom: toggle, Overview, modules, then the account control", async () => {
await render();
const focusable = [
...sidebar().querySelectorAll<HTMLElement>("a[href], button:not([disabled])"),
].map((element) => element.dataset.testid);
expect(focusable).toEqual([
"sidebar-toggle",
"select-overview",
"select-signage",
"select-calendar",
"host-account-menu",
]);
});
it("gives every navigation item a pointer and touch target of at least the medium control height", async () => {
await render();
for (const id of ["overview", "signage", "calendar"]) {
expect(navLink(id).style.minHeight, id).toBe("var(--ekklesia-control-height-md)");
}
});
});
describe("the account area", () => {
it("is anchored at the bottom of the sidebar as a product-owned menu button", async () => {
await render();
const trigger = required("host-account-menu");
expect(sidebar().lastElementChild?.contains(trigger)).toBe(true);
expect(trigger.tagName).toBe("BUTTON");
expect(trigger.getAttribute("aria-haspopup")).toBe("menu");
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(trigger.textContent).toContain("Your account");
// Only the opaque user id is known to the shell, and it is never shown.
expect(container.textContent).not.toContain("user-1");
expect(query("host-account-menu-list")).toBeNull();
});
it("opens on activation with focus on its first item, and offers only actions that work today", async () => {
await render();
const trigger = required("host-account-menu");
await click(trigger);
const menu = required("host-account-menu-list");
expect(menu.getAttribute("role")).toBe("menu");
expect(trigger.getAttribute("aria-expanded")).toBe("true");
expect(trigger.getAttribute("aria-controls")).toBe(menu.id);
const items = [...menu.querySelectorAll('[role="menuitem"]')];
expect(items.map((item) => item.textContent)).toEqual(["Sign out"]);
expect(document.activeElement).toBe(items[0]);
expect(menu.querySelector("a")).toBeNull();
});
it("opens from the keyboard with ArrowDown, and Escape closes it and returns focus", async () => {
await render();
const trigger = required("host-account-menu");
await focus(trigger);
await key(trigger, "ArrowDown");
expect(document.activeElement).toBe(required("host-logout"));
await key(required("host-logout"), "Escape");
expect(query("host-account-menu-list")).toBeNull();
expect(document.activeElement).toBe(trigger);
});
it("closes when focus or a pointer leaves it, without signing anyone out", async () => {
const { platform: stub } = await render();
await click(required("host-account-menu"));
await act(async () => {
document.body.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
});
expect(query("host-account-menu-list")).toBeNull();
await click(required("host-account-menu"));
await key(required("host-logout"), "Tab");
expect(query("host-account-menu-list")).toBeNull();
expect(stub.calls.some((url) => url.endsWith("/api/auth/sign-out"))).toBe(false);
});
it("signs out from the menu, back to the sign-in form", async () => {
const { platform: stub } = await render({ path: "/calendar" });
await click(required("host-account-menu"));
await click(required("host-logout"));
expect(query("host-login")).not.toBeNull();
expect(query("host-sidebar")).toBeNull();
expect(stub.calls.some((url) => url.endsWith("/api/auth/sign-out"))).toBe(true);
});
it("never uses a browser-native dialog", async () => {
const alert = vi.fn();
const confirm = vi.fn(() => true);
const prompt = vi.fn(() => null);
vi.stubGlobal("alert", alert);
vi.stubGlobal("confirm", confirm);
vi.stubGlobal("prompt", prompt);
try {
await render({ compact: true });
await click(required("mobile-navigation-trigger"));
await key(navLink("overview"), "Escape");
await click(required("mobile-navigation-trigger"));
await click(required("host-account-menu"));
await click(required("host-logout"));
expect(alert).not.toHaveBeenCalled();
expect(confirm).not.toHaveBeenCalled();
expect(prompt).not.toHaveBeenCalled();
} finally {
vi.unstubAllGlobals();
}
});
});
describe("phone navigation drawer", () => {
it("replaces the sidebar with one labelled menu trigger, and renders no navigation until opened", async () => {
await render({ compact: true });
expect(query("host-sidebar")).toBeNull();
const trigger = required("mobile-navigation-trigger");
expect(trigger.tagName).toBe("BUTTON");
expect(trigger.getAttribute("aria-label")).toBe("Open navigation");
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(trigger.getAttribute("aria-controls")).toBe("ekklesia-navigation-drawer");
expect(query("navigation-drawer")).toBeNull();
expect(query("host-overview")).not.toBeNull();
});
it("opens as a modal dialog with the same navigation, focus inside, and the page inert", async () => {
await render({ compact: true, path: "/calendar" });
const trigger = required("mobile-navigation-trigger");
await click(trigger);
const drawer = required("navigation-drawer");
expect(drawer.id).toBe("ekklesia-navigation-drawer");
expect(drawer.getAttribute("role")).toBe("dialog");
expect(drawer.getAttribute("aria-modal")).toBe("true");
expect(drawer.getAttribute("aria-label")).toBe("Navigation");
expect(trigger.getAttribute("aria-expanded")).toBe("true");
expect(document.activeElement).toBe(required("navigation-drawer-close"));
expect([...drawer.querySelectorAll("nav a")].map((link) => link.textContent)).toEqual([
"Overview",
"Signage",
"Calendar",
]);
// The same hierarchy and the same current page as on desktop, always with labels.
expect(navLink("calendar").getAttribute("aria-current")).toBe("page");
expect(drawer.querySelector(".ek-shell-visually-hidden[data-nav-label]")).toBeNull();
expect(required("host-main-region").hasAttribute("inert")).toBe(true);
expect(drawer.contains(required("host-account-menu"))).toBe(true);
});
it("closes on Escape and returns focus to the trigger", async () => {
await render({ compact: true });
const trigger = required("mobile-navigation-trigger");
await click(trigger);
await key(navLink("signage"), "Escape");
expect(query("navigation-drawer")).toBeNull();
expect(document.activeElement).toBe(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(required("host-main-region").hasAttribute("inert")).toBe(false);
});
it("closes from its close button and from the backdrop, returning focus each time", async () => {
await render({ compact: true });
const trigger = required("mobile-navigation-trigger");
await click(trigger);
await click(required("navigation-drawer-close"));
expect(query("navigation-drawer")).toBeNull();
expect(document.activeElement).toBe(trigger);
await click(trigger);
await click(required("navigation-drawer-backdrop"));
expect(query("navigation-drawer")).toBeNull();
expect(document.activeElement).toBe(trigger);
});
it("traps Tab and Shift+Tab inside itself while open", async () => {
await render({ compact: true });
await click(required("mobile-navigation-trigger"));
const drawer = required("navigation-drawer");
const focusable = [...drawer.querySelectorAll<HTMLElement>("a[href], button:not([disabled])")];
const first = focusable[0] as HTMLElement;
const last = focusable.at(-1) as HTMLElement;
expect(first).toBe(required("navigation-drawer-close"));
expect(last).toBe(required("host-account-menu"));
await focus(last);
const forward = await key(last, "Tab");
expect(forward.defaultPrevented).toBe(true);
expect(document.activeElement).toBe(first);
const backward = await key(first, "Tab", { shiftKey: true });
expect(backward.defaultPrevented).toBe(true);
expect(document.activeElement).toBe(last);
// Tab between two inner items is left to the browser.
await focus(navLink("overview"));
expect((await key(navLink("overview"), "Tab")).defaultPrevented).toBe(false);
});
it("closes when a destination is chosen, opens that page, and puts focus on the page", async () => {
const { history } = await render({ compact: true });
await click(required("mobile-navigation-trigger"));
await click(navLink("signage"));
expect(query("navigation-drawer")).toBeNull();
expect(history.entries).toEqual(["/", "/signage"]);
expect(isolatedText(query("module-container"))).toBe("signage mounted");
expect(document.activeElement).toBe(required("host-main-region"));
});
it("closes when the page already open is chosen, and still moves focus to the page", async () => {
const { history } = await render({ compact: true, path: "/calendar" });
await click(required("mobile-navigation-trigger"));
await click(navLink("calendar"));
expect(query("navigation-drawer")).toBeNull();
expect(history.entries).toEqual(["/calendar"]);
expect(document.activeElement).toBe(required("host-main-region"));
});
it("closes on Back and Forward, which keep working with the drawer", async () => {
const { history } = await render({ compact: true });
await click(required("mobile-navigation-trigger"));
await click(navLink("calendar"));
await click(required("mobile-navigation-trigger"));
await act(async () => {
history.go(-1);
});
expect(query("navigation-drawer")).toBeNull();
expect(query("host-overview")).not.toBeNull();
await act(async () => {
history.go(1);
});
expect(isolatedText(query("module-container"))).toBe("calendar mounted");
});
it("signs out from the account menu inside the drawer", async () => {
await render({ compact: true });
await click(required("mobile-navigation-trigger"));
await click(required("host-account-menu"));
await click(required("host-logout"));
expect(query("host-login")).not.toBeNull();
});
it("gives drawer items a touch target of at least the large control height", async () => {
await render({ compact: true });
await click(required("mobile-navigation-trigger"));
for (const id of ["overview", "signage", "calendar"]) {
expect(navLink(id).style.minHeight, id).toBe("var(--ekklesia-control-height-lg)");
}
expect(required("mobile-navigation-trigger").style.minHeight).toBe(
"var(--ekklesia-control-height-lg)",
);
});
});
describe("moving between phone and desktop layouts", () => {
it("keeps the open page and the desktop mode, and drops an open drawer cleanly", async () => {
const { viewport, history } = await render({
compact: true,
path: "/signage",
preference: createMemorySidebarPreference("collapsed"),
});
await click(required("mobile-navigation-trigger"));
await focus(navLink("calendar"));
await act(async () => {
viewport.setCompact(false);
});
expect(query("navigation-drawer")).toBeNull();
expect(sidebar().dataset.sidebar).toBe("collapsed");
expect(navLink("signage").getAttribute("aria-current")).toBe("page");
expect(history.entries).toEqual(["/signage"]);
expect(isolatedText(query("module-container"))).toBe("signage mounted");
expect(required("host-main-region").hasAttribute("inert")).toBe(false);
// Focus was inside the drawer that went away; it lands on the page rather than nowhere.
expect(document.activeElement).toBe(required("host-main-region"));
await act(async () => {
viewport.setCompact(true);
});
expect(query("host-sidebar")).toBeNull();
expect(query("navigation-drawer")).toBeNull();
expect(required("mobile-navigation-trigger").getAttribute("aria-expanded")).toBe("false");
});
it("does not remount the open module when the layout changes", async () => {
const mounts: string[] = [];
const viewport = createMemoryHostViewport(false);
const history = createMemoryHostHistory("/signage");
const stub = platform();
await act(async () => {
root.render(
<HostApp
config={{ apiBaseUrl, fetch: stub.fetch }}
history={history}
importModule={async (url) => {
mounts.push(url);
return artifactFor(url);
}}
sidebarPreference={createMemorySidebarPreference()}
viewport={viewport}
/>,
);
});
await act(async () => {
viewport.setCompact(true);
});
await act(async () => {
viewport.setCompact(false);
});
expect(mounts).toHaveLength(1);
expect(isolatedText(query("module-container"))).toBe("signage mounted");
});
});