Files
Ekklesia/packages/module-ui-sdk/tests/navigation.test.ts
ekklesiaandClaude Opus 5.5 c78f1318b4
CI / verify (pull_request) Successful in 1m11s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m27s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 52s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 3m35s
Make the module navigation capability an optional context member
ModuleUiHostContext.navigation is now `navigation?: ModuleUiNavigation |
undefined`, so the public context change is source-compatible: a context built
before navigation existed (without the member) still compiles unchanged, as
0.x patch releases require. The Dashboard supplies navigation; hosts without
addressable pages, including the display host, omit the member; modules
feature-detect it. Existing hosts' tests are restored to their pre-PR shape,
and a module-ui-sdk test proves a pre-navigation context still satisfies
ModuleUiHostContext (and fails if the member is ever made required).
Runtime behaviour, manifest v8/v9 compatibility, and security rules are
unchanged. ADR 0063 and the module development guide say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 09:13:03 -05:00

64 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
type ModuleUiHostContext,
type ModuleUiNavigation,
ModuleUiNavigationError,
NO_MODULE_UI_CONTRIBUTIONS,
} from "../src/index.js";
/** The navigation capability's public surface (ADR 0063). Its behaviour is the host's: the
* Dashboard's implementation is proven in `surfaces/shell/tests/module-navigation.test.tsx`. */
describe("module UI navigation contract", () => {
it("reports a refused path with a stable code and a fixed message that never echoes the path", () => {
for (const code of [
"MODULE_NAVIGATION_PATH_INVALID",
"MODULE_NAVIGATION_PATH_UNKNOWN",
] as const) {
const error = new ModuleUiNavigationError(code);
expect(error).toBeInstanceOf(Error);
expect(error.name).toBe("ModuleUiNavigationError");
expect(error.code).toBe(code);
expect(error.stack).toBe(`ModuleUiNavigationError: ${error.message}`);
expect(error.message).not.toMatch(/[/:?#]/u);
}
});
it("keeps a context built before navigation existed a complete, valid context", () => {
// Exactly the seven members every host supplied before ADR 0063, with no `navigation` member at
// all. This must keep compiling unchanged: `tsc` over this file is the compatibility proof, and
// `satisfies` makes the check exact rather than structural-by-accident.
const preNavigationContext = {
api: { request: async () => new Response(null, { status: 204 }) },
contributions: NO_MODULE_UI_CONTRIBUTIONS,
display: undefined,
moduleId: "announcements",
permissions: { has: () => false },
principal: { kind: "anonymous" },
realtime: { subscribe: () => ({ close: () => undefined }) },
} satisfies ModuleUiHostContext;
const asContext: ModuleUiHostContext = preNavigationContext;
expect("navigation" in asContext).toBe(false);
expect(asContext.navigation?.location().path).toBeUndefined();
});
it("lets a host supply navigation, omit it, or set it to undefined, and a module feature-detect it", () => {
const navigation: ModuleUiNavigation = {
link: () => ({ href: "/m/x", onClick: () => undefined }),
location: () => ({ page: "x", path: "x" }),
navigate: () => undefined,
settings: undefined,
subscribe: () => () => undefined,
};
const where = (context: Pick<ModuleUiHostContext, "navigation">): string =>
context.navigation?.location().path ?? "no navigation";
expect(where({ navigation })).toBe("x");
expect(where({})).toBe("no navigation");
expect(where({ navigation: undefined })).toBe("no navigation");
// Present, it must be the real capability — not any object.
// @ts-expect-error a navigation member must satisfy ModuleUiNavigation
expect(where({ navigation: { location: () => "x" } })).toBeDefined();
});
});