Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/services/command-registry/commandRegistry.test.ts
T

313 lines
12 KiB
TypeScript

import { NavigateFunction } from "react-router";
import { describe, expect, it, vi } from "vitest";
import { getCommandRegistryActions, getSidebarCommandActions } from "./commandRegistry";
const t = vi.fn().mockImplementation((value: string) => value) as any;
const routing = {
protocol: "https",
webAppHost: "app.example.test",
adminAppHost: "admin.example.test",
apiHost: "api.example.test",
appBaseDomain: "example.test",
};
function rootData(tenantApiKeys = true, admin = false, routingConfig?: typeof routing, crm = false) {
return {
appConfiguration: {
app: { features: { tenantApiKeys, crm } },
portals: { enabled: false, forTenants: false },
},
user: { admin },
routing: routingConfig,
} as any;
}
function appData(options?: { admin?: boolean; permissions?: string[]; tenantApiKeys?: boolean }) {
return {
currentTenant: { id: "tenant_1", slug: "acme" },
entities: [
{ name: "company", titlePlural: "models.company.plural", slug: "companies", showInSidebar: true },
{ name: "contact", titlePlural: "models.contact.plural", slug: "contacts", showInSidebar: true },
{ name: "opportunity", titlePlural: "models.opportunity.plural", slug: "opportunities", showInSidebar: true },
],
myTenants: [{ id: "tenant_2", slug: "other" }],
permissions: options?.permissions ?? [
"app.settings.members.view",
"app.settings.subscription.view",
"app.settings.account.view",
"app.settings.apiKeys.view",
"app.settings.auditTrails.view",
"entity.company.view",
"entity.contact.view",
"entity.opportunity.view",
],
isSuperAdmin: false,
user: { id: "user_1", admin: options?.admin ?? true },
} as any;
}
function navigateMock() {
return vi.fn() as unknown as NavigateFunction & { mock: ReturnType<typeof vi.fn>["mock"] };
}
function perform(action: { perform?: (action: any) => void; id: string }) {
action.perform?.({ id: action.id });
}
function expectNoLegacyPrefixes(targets: string[]) {
targets.forEach((target) => {
expect(target).not.toMatch(/^\/app\//);
expect(target).not.toMatch(/^\/admin(?:\/|$)/);
});
}
describe("command registry", () => {
it("builds app tenant commands with host-aware tenant URLs and scope metadata", () => {
const navigate = navigateMock();
const actions = getCommandRegistryActions({ layout: "app", t, navigate, appData: appData(), rootData: rootData() });
perform(actions.find((action) => action.id === "dashboard")!);
perform(actions.find((action) => action.id === "members")!);
perform(actions.find((action) => action.id === "apiKeys")!);
expect(navigate).toHaveBeenNthCalledWith(1, "/acme/dashboard");
expect(navigate).toHaveBeenNthCalledWith(2, "/acme/settings/members");
expect(navigate).toHaveBeenNthCalledWith(3, "/acme/settings/api");
expect(actions.find((action) => action.id === "members")?.scope).toMatchObject({
layout: "app",
tenantSlug: "acme",
permission: "app.settings.members.view",
});
expect(actions.find((action) => action.id === "apiKeys")?.scope).toMatchObject({ featureFlag: "tenantApiKeys", permission: "app.settings.apiKeys.view" });
expectNoLegacyPrefixes(navigate.mock.calls.map(([target]) => target));
});
it("omits app commands for empty app data", () => {
expect(getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), rootData: rootData() })).toEqual([]);
});
it("filters tenant settings commands by permissions and feature flags", () => {
const actions = getCommandRegistryActions({
layout: "app",
t,
navigate: navigateMock(),
appData: appData({ permissions: ["app.settings.account.view"] }),
rootData: rootData(false),
});
expect(actions.some((action) => action.id === "account")).toBe(true);
expect(actions.some((action) => action.id === "members")).toBe(false);
expect(actions.some((action) => action.id === "subscription")).toBe(false);
expect(actions.some((action) => action.id === "apiKeys")).toBe(false);
expect(actions.some((action) => action.id === "auditTrails")).toBe(false);
expect(actions.some((action) => action.id === "events")).toBe(false);
});
it("filters tenant entity commands through permission-aware scope metadata", () => {
const actions = getCommandRegistryActions({
layout: "app",
t,
navigate: navigateMock(),
appData: appData({ permissions: ["entity.company.view"] }),
rootData: rootData(),
});
expect(actions.find((action) => action.id === "entity-company")?.scope).toMatchObject({
layout: "app",
tenantSlug: "acme",
permission: "entity.company.view",
});
expect(actions.some((action) => action.id === "entity-contact")).toBe(false);
expect(actions.some((action) => action.id === "entity-opportunity")).toBe(false);
});
it("omits CRM command while the CRM route tree is disabled", () => {
const disabled = getCommandRegistryActions({
layout: "app",
t,
navigate: navigateMock(),
appData: appData(),
rootData: rootData(true, true, routing, false),
});
const enabled = getCommandRegistryActions({
layout: "app",
t,
navigate: navigateMock(),
appData: appData(),
rootData: rootData(true, true, routing, true),
});
expect(disabled.some((action) => action.id === "crm")).toBe(false);
expect(enabled.some((action) => action.id === "crm")).toBe(false);
});
it("keeps platform commands available when scoped tenant commands are denied", () => {
const actions = getCommandRegistryActions({
layout: "app",
t,
navigate: navigateMock(),
appData: appData({ admin: false, permissions: [] }),
rootData: rootData(),
});
expect(actions.some((action) => action.id === "entity-company")).toBe(false);
expect(actions.some((action) => action.id === "create account")).toBe(true);
expect(actions.some((action) => action.id === "logout")).toBe(true);
});
it("omits switch-to-admin for non-admin app users", () => {
const actions = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData({ admin: false }), rootData: rootData() });
expect(actions.some((action) => action.id === "switch to admin")).toBe(false);
});
it("switches app admins to the admin dashboard instead of the admin host root", () => {
const navigate = navigateMock();
const actions = getCommandRegistryActions({ layout: "app", t, navigate, appData: appData({ admin: true }), rootData: rootData(true, true, routing) });
vi.stubGlobal("window", undefined);
try {
perform(actions.find((action) => action.id === "switch to admin")!);
} finally {
vi.unstubAllGlobals();
}
expect(navigate).toHaveBeenCalledWith("https://admin.example.test/dashboard");
});
it("filters admin sidebar commands for non-superadmin contexts while preserving platform commands", () => {
const actions = getCommandRegistryActions({ layout: "admin", t, navigate: navigateMock(), rootData: rootData() });
expect(actions.some((action) => action.id === "/dashboard")).toBe(false);
expect(actions.some((action) => action.id === "create account")).toBe(true);
expect(actions.some((action) => action.id === "logout")).toBe(true);
});
it("builds admin commands when app data has no current tenant", () => {
const actions = getCommandRegistryActions({
layout: "admin",
t,
navigate: navigateMock(),
appData: { ...appData({ admin: true }), currentTenant: null, isSuperAdmin: true } as any,
rootData: rootData(true, true),
});
expect(actions.some((action) => action.id === "/dashboard")).toBe(true);
expect(actions.every((action) => action.scope.layout === "admin")).toBe(true);
});
it("builds admin commands with host-aware paths and app fallback switch", () => {
const navigate = navigateMock();
const actions = getCommandRegistryActions({ layout: "admin", t, navigate, rootData: rootData(true, true) });
perform(actions.find((action) => action.id === "/dashboard")!);
perform(actions.find((action) => action.id === "/settings/profile")!);
perform(actions.find((action) => action.name === "admin.switchToApp")!);
expect(navigate).toHaveBeenNthCalledWith(1, "/dashboard");
expect(navigate).toHaveBeenNthCalledWith(2, "/settings/profile");
expect(navigate).toHaveBeenNthCalledWith(3, "/");
expect(actions.find((action) => action.id === "/dashboard")?.scope).toMatchObject({ layout: "admin", platform: true, adminOnly: true });
expect(actions.find((action) => action.name === "admin.switchToApp")?.scope).toMatchObject({ layout: "admin", platform: true, adminOnly: true });
expectNoLegacyPrefixes(navigate.mock.calls.map(([target]) => target));
});
it("does not expose expandable admin sidebar parents as command destinations", () => {
const actions = getCommandRegistryActions({
layout: "admin",
t,
navigate: navigateMock(),
appData: {
...appData({
admin: true,
permissions: ["admin.accounts.view", "admin.settings.view"],
}),
currentTenant: null,
isSuperAdmin: true,
} as any,
rootData: rootData(true, true),
});
expect(actions.some((action) => action.id === "/accounts" && action.name === "app.sidebar.accountsAndUsers")).toBe(false);
expect(actions.some((action) => action.id === "/settings" && action.name === "app.sidebar.settings")).toBe(false);
expect(actions.some((action) => action.id === "/accounts" && action.name === "admin.tenants.title")).toBe(true);
expect(actions.some((action) => action.id === "/settings/profile")).toBe(true);
});
it("does not expose disabled sidebar items as command destinations", () => {
const actions = getSidebarCommandActions(
[
{
title: "Settings",
path: "/settings",
items: [
{ title: "Profile", path: "/settings/profile" },
{ title: "Disabled Plugin", path: "/settings/plugin", disabled: true },
],
},
],
"admin"
);
expect(actions.some((action) => action.id === "/settings/profile")).toBe(true);
expect(actions.some((action) => action.id === "/settings/plugin")).toBe(false);
});
it("uses parent sidebar titles as command subtitles", () => {
const actions = getSidebarCommandActions(
[
{
title: "Settings",
path: "/settings",
items: [{ title: "Profile", path: "/settings/profile" }],
},
],
"admin"
);
expect(actions.find((action) => action.id === "/settings/profile")?.subtitle).toBe("Settings");
});
it("builds admin public-app commands with absolute app-host URLs when routing is configured", () => {
const navigate = navigateMock();
const actions = getCommandRegistryActions({ layout: "admin", t, navigate, rootData: rootData(true, true, routing) });
expect(actions.find((action) => action.name === "admin.switchToApp")?.id).toBe("https://app.example.test/");
vi.stubGlobal("window", undefined);
try {
perform(actions.find((action) => action.name === "admin.switchToApp")!);
perform(actions.find((action) => action.id === "create account")!);
} finally {
vi.unstubAllGlobals();
}
expect(navigate).toHaveBeenNthCalledWith(1, "https://app.example.test/");
expect(navigate).toHaveBeenNthCalledWith(2, "https://app.example.test/new-account");
});
it("passes docs commands through with registry scope and navigation", () => {
const navigate = navigateMock();
const actions = getCommandRegistryActions({ layout: "docs", t, navigate });
const firstAction = actions[0];
perform(firstAction);
expect(firstAction.scope).toMatchObject({ layout: "docs", platform: true });
expect(navigate).toHaveBeenCalledWith(firstAction.id);
});
it("exposes app, admin, and docs commands directly through the registry", () => {
expect(getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData(), rootData: rootData() })[0]).toMatchObject({
id: "dashboard",
scope: { layout: "app" },
});
expect(getCommandRegistryActions({ layout: "admin", t, navigate: navigateMock(), rootData: rootData(true, true) })[0]).toMatchObject({
id: "/dashboard",
scope: { layout: "admin" },
});
expect(getCommandRegistryActions({ layout: "docs", t, navigate: navigateMock() })[0]).toMatchObject({ scope: { layout: "docs" } });
});
});