Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/application/sidebar/AdminSidebar.test.tsx
T

317 lines
14 KiB
TypeScript

import { readdirSync, statSync } from "node:fs";
import * as path from "node:path";
import { describe, it, expect, vi } from "vitest";
import { AdminSidebar } from "./AdminSidebar";
import type { SideBarItem } from "./SidebarItem";
import { UI_NAV_IDS } from "~/application/ui/uiRegistry";
import { canonicalAdminRouteExactPaths, canonicalAdminRouteSlugs, disabledPluginRouteSlugs } from "~/utils/app/PublicRouteSegments";
import { resolveRepoPath } from "~/utils/testing/repoPaths";
function flattenSidebarItems(items: SideBarItem[]): SideBarItem[] {
return items.flatMap((item) => [item, ...flattenSidebarItems(item.items ?? [])]);
}
function walkRouteFiles(directory: string): string[] {
return readdirSync(directory).flatMap((entry) => {
const absolutePath = path.join(directory, entry);
const stats = statSync(absolutePath);
if (stats.isDirectory()) {
return walkRouteFiles(absolutePath);
}
if (!/\.(tsx?|jsx?)$/.test(entry) || /\.(test|spec)\.(tsx?|jsx?)$/.test(entry) || entry.endsWith(".server.ts")) {
return [];
}
return [absolutePath];
});
}
function routePathFromAdminRouteFile(absolutePath: string): string | null {
const routeRoot = resolveRepoPath("apps/web/app/routes/admin");
const relativePath = path.relative(routeRoot, absolutePath).replace(/\\/g, "/");
if (relativePath === "404.tsx" || relativePath.startsWith("internal/")) {
return null;
}
const withoutExtension = relativePath.replace(/\.(tsx?|jsx?)$/, "");
const parts = withoutExtension
.split("/")
.flatMap((part) => part.split("."))
.filter((part) => part && part !== "index" && !part.startsWith("__"));
return parts.join("/");
}
function collectAdminRoutePaths(): string[] {
return walkRouteFiles(resolveRepoPath("apps/web/app/routes/admin"))
.map(routePathFromAdminRouteFile)
.filter((routePath): routePath is string => routePath !== null);
}
function targetMatchesRoute(target: string, routePath: string): boolean {
const targetParts = target.split("/").filter(Boolean);
const routeParts = routePath.split("/").filter(Boolean);
if (targetParts.length !== routeParts.length) {
return false;
}
return routeParts.every((routePart, index) => routePart.startsWith("$") || routePart === targetParts[index]);
}
function normalizeSidebarPath(pathname: string): string | null {
if (!pathname.startsWith("/") || pathname === "/") {
return null;
}
return pathname.split(/[?#]/)[0]?.replace(/^\/+|\/+$/g, "") || null;
}
describe("AdminSidebar", () => {
const t = vi.fn().mockImplementation((v: string) => v) as any;
it("returns an array of sidebar items", () => {
const sidebar = AdminSidebar({ t, appConfiguration: null });
const serialized = JSON.stringify(sidebar);
const allPaths = flattenSidebarItems(sidebar)
.map((item) => item.path)
.filter((path): path is string => Boolean(path) && path !== "/" && path.startsWith("/"));
expect(Array.isArray(sidebar)).toBe(true);
expect(serialized).toContain("/dashboard");
expect(serialized).toContain("/accounts");
expect(serialized).toContain("/audit-trails");
expect(serialized).toContain("/notifications/messages");
expect(serialized).not.toContain("/feature-flags");
expect(serialized).toContain("/settings/security");
expect(serialized).not.toContain("/settings/profile");
expect(serialized).not.toContain("/metrics");
expect(sidebar).toEqual(
expect.arrayContaining([
expect.objectContaining({
items: expect.arrayContaining([expect.objectContaining({ id: "ui.nav.admin.dashboard", path: "/dashboard" })]),
}),
])
);
expect(sidebar.at(-1)?.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: "ui.nav.admin.switchToApp", path: "/select-tenant", permission: "admin.accounts.view", exact: true }),
])
);
const emittedPaths = serialized.match(/"path":"[^"]*"/g) ?? [];
expect(emittedPaths.filter((path) => path.includes("/admin/"))).toEqual([]);
expect(serialized).not.toContain("/admin/pages");
expect(serialized).not.toContain("/admin/blog");
expect(serialized).not.toContain("/admin/crons");
expect(serialized).not.toContain("/email-marketing");
expect(serialized).not.toContain("/forms");
expect(serialized).not.toContain("/help-desk");
expect(serialized).not.toContain("/knowledge-base");
expect(serialized).not.toContain("/portals");
expect(serialized).not.toContain("/prompts");
expect(serialized).not.toContain("/workflow-engine");
expect(serialized).not.toContain('"path":"/crm"');
expect(serialized).not.toContain("ui.nav.admin.crm");
expect(allPaths.map((path) => path.split("/").filter(Boolean)[0])).not.toEqual(expect.arrayContaining([...disabledPluginRouteSlugs]));
expect(allPaths.map((path) => path.split("/").filter(Boolean)[0])).toEqual(expect.arrayContaining(["dashboard", "accounts", "settings"]));
allPaths.forEach((path) => {
const firstSlug = path.split("/").filter(Boolean)[0] ?? "";
const isCanonicalSlug = (canonicalAdminRouteSlugs as readonly string[]).includes(firstSlug);
const isCanonicalExactPath = (canonicalAdminRouteExactPaths as readonly string[]).includes(path);
expect(isCanonicalSlug || isCanonicalExactPath, path).toBe(true);
});
sidebar.forEach((item) => {
expect(typeof item.title).toBe("string");
expect(typeof item.path).toBe("string");
if (item.items) {
expect(Array.isArray(item.items)).toBe(true);
item.items.forEach((subItem) => {
expect(typeof subItem.title).toBe("string");
expect(typeof subItem.path).toBe("string");
});
}
});
});
it("consolidates platform operations and support without exposing feature flags", () => {
const sidebar = AdminSidebar({ t, appConfiguration: null });
const platform = sidebar.find((item) => item.id === "ui.nav.admin.platform");
const platformItems = platform?.items ?? [];
const operations = platformItems.find((item) => item.id === "ui.nav.admin.platform.operations");
const support = platformItems.find((item) => item.id === "ui.nav.admin.platform.support");
expect(platformItems.map((item) => item.id)).toEqual(["ui.nav.admin.platform.operations", "ui.nav.admin.platform.support", UI_NAV_IDS.adminApi]);
expect(operations).toMatchObject({
title: "admin.platformNavigation.operations",
path: "/system",
isCollapsible: false,
activePaths: ["/system", "/system/health", "/system/deploys", "/system/jobs", "/system/webhooks", "/system/billing"],
anyPermissions: ["admin.metrics.view", "admin.events.view", "admin.account.subscription"],
});
expect(operations?.items?.map((item) => item.path)).toEqual([
"/system",
"/system/health",
"/system/deploys",
"/system/jobs",
"/system/webhooks",
"/system/billing",
]);
const accounts = flattenSidebarItems(sidebar).find((item) => item.id === UI_NAV_IDS.adminAccounts);
const audit = flattenSidebarItems(sidebar).find((item) => item.id === UI_NAV_IDS.adminAudit);
expect(accounts?.items?.map((item) => item.path)).not.toContain("/accounts/billing");
expect(accounts?.excludePaths).toContain("/accounts/billing");
expect(audit?.items?.map((item) => item.path)).not.toContain("/webhooks");
expect(support).toMatchObject({
title: "admin.platformNavigation.support",
path: "/support",
anyPermissions: ["admin_identity.impersonation.start", "admin.metrics.view"],
isCollapsible: false,
activePaths: ["/support", "/support/kwp-contracts"],
});
expect(support?.items?.map((item) => item.path)).toEqual(["/support", "/support/kwp-contracts"]);
expect(platformItems.at(-1)).toMatchObject({
id: UI_NAV_IDS.adminApi,
title: "admin.platformNavigation.apiAccess",
path: "/api",
});
expect(flattenSidebarItems(sidebar).map((item) => item.id)).not.toContain(UI_NAV_IDS.adminFeatureFlags);
});
it("gates platform leads only by the sales CRM read permission", () => {
const leadInbox = flattenSidebarItems(AdminSidebar({ t, appConfiguration: null })).find((item) => item.id === "ui.nav.admin.platformLeads");
expect(leadInbox).toMatchObject({
permission: "admin_sales.crm.read",
});
});
it("keeps sales work and communication separate while nesting configuration under settings", () => {
const sidebar = AdminSidebar({ t, appConfiguration: null });
const salesGroups = sidebar.filter((item) => ["ui.nav.admin.sales", "ui.nav.admin.salesCommunication"].includes(item.id ?? ""));
const settings = flattenSidebarItems(sidebar).find((item) => item.id === UI_NAV_IDS.adminSettings);
expect(salesGroups.map((group) => group.id)).toEqual(["ui.nav.admin.sales", "ui.nav.admin.salesCommunication"]);
expect(salesGroups.map((group) => group.title)).toEqual(["admin.sales.navigation.title", "admin.sales.navigation.communicationSection"]);
expect(salesGroups[0]?.items?.map((item) => item.id)).toEqual([
"ui.nav.admin.sales.today",
"ui.nav.admin.platformLeads",
"ui.nav.admin.sales.deals",
"ui.nav.admin.sales.contacts",
"ui.nav.admin.sales.companies",
UI_NAV_IDS.adminSalesImportExport,
]);
expect(salesGroups[1]?.items?.map((item) => item.id)).toEqual(["ui.nav.admin.sales.email", "ui.nav.admin.sales.calls", "ui.nav.admin.sales.bookings"]);
expect(salesGroups.flatMap((group) => group.items ?? []).every((item) => item.icon)).toBe(true);
expect(settings?.items?.slice(-4).map((item) => item.id)).toEqual([
UI_NAV_IDS.adminSalesPipelineSettings,
UI_NAV_IDS.adminSalesLeadSettings,
UI_NAV_IDS.adminSalesFieldsTags,
UI_NAV_IDS.adminSalesTelephonySettings,
]);
expect(settings).toMatchObject({
isCollapsible: false,
activePaths: ["/settings"],
anyPermissions: expect.arrayContaining(["admin_identity.sessions.read", "admin.settings.general.view", "admin.settings.danger.reset"]),
});
expect(settings?.items?.slice(0, 3).map((item) => item.id)).toEqual([
"ui.nav.admin.settings.profile",
"ui.nav.admin.settings.adminUsers",
"ui.nav.admin.settings.adminRoles",
]);
expect(flattenSidebarItems(sidebar).map((item) => item.id)).not.toContain("ui.nav.admin.adminUsers");
expect(flattenSidebarItems(sidebar).map((item) => item.id)).not.toContain("ui.nav.admin.salesConfiguration");
});
it("gates lead automation settings with the smallest assignment permission", () => {
const settings = flattenSidebarItems(AdminSidebar({ t, appConfiguration: null })).find((item) => item.id === "ui.nav.admin.sales.leadSettings");
expect(settings).toMatchObject({
path: "/settings/leads",
permission: "admin_sales.crm.assign",
});
});
it("exposes the personal today list with CRM read permission", () => {
const today = flattenSidebarItems(AdminSidebar({ t, appConfiguration: null })).find((item) => item.id === "ui.nav.admin.sales.today");
expect(today).toMatchObject({
path: "/sales/tasks/today",
permission: "admin_sales.crm.read",
});
});
it("exposes telephony workspace and settings with their smallest permissions", () => {
const items = flattenSidebarItems(AdminSidebar({ t, appConfiguration: null }));
expect(items.find((item) => item.id === "ui.nav.admin.sales.calls")).toMatchObject({
path: "/sales/calls",
permission: "admin_sales.phone.call",
});
expect(items.find((item) => item.id === "ui.nav.admin.sales.telephonySettings")).toMatchObject({
path: "/settings/telephony",
permission: "admin_sales.integrations.manage",
});
});
it("exposes tenantless platform contacts with CRM read permission", () => {
const contacts = flattenSidebarItems(AdminSidebar({ t, appConfiguration: null })).find((item) => item.id === "ui.nav.admin.sales.contacts");
expect(contacts).toMatchObject({
path: "/sales/contacts",
permission: "admin_sales.crm.read",
});
});
it("exposes every VA-P28b data screen through its smallest permission", () => {
const items = flattenSidebarItems(AdminSidebar({ t, appConfiguration: null }));
expect(items.find((item) => item.id === "ui.nav.admin.sales.companies")).toMatchObject({
path: "/sales/companies",
permission: "admin_sales.crm.read",
});
expect(items.find((item) => item.id === "ui.nav.admin.sales.pipelineSettings")).toMatchObject({
path: "/settings/pipelines",
permission: "admin_sales.crm.write",
});
expect(items.find((item) => item.id === "ui.nav.admin.sales.fieldsTags")).toMatchObject({
path: "/settings/fields-tags",
permission: "admin_sales.crm.read",
});
expect(items.find((item) => item.id === "ui.nav.admin.sales.importExport")).toMatchObject({
path: "/sales/import-export",
permission: "admin_sales.crm.read",
});
});
it("only emits canonical admin sidebar paths backed by route files", () => {
const routePaths = collectAdminRoutePaths();
const sidebar = AdminSidebar({
t,
appConfiguration: {
app: {
features: {
tenantApiKeys: true,
},
},
} as any,
});
const sidebarTargets = flattenSidebarItems(sidebar)
.map((item) => item.path)
.filter((pathname): pathname is string => Boolean(pathname))
.map(normalizeSidebarPath)
.filter((pathname): pathname is string => pathname !== null);
expect(sidebarTargets).not.toContain("admin");
expect(sidebarTargets.map((pathname) => pathname.split("/")[0])).not.toEqual(expect.arrayContaining([...disabledPluginRouteSlugs]));
const missingTargets = sidebarTargets.filter((target) => !routePaths.some((routePath) => targetMatchesRoute(target, routePath)));
expect(missingTargets).toEqual([]);
});
});