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

167 lines
6.3 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 { 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("/analytics/overview");
expect(serialized).toContain("/audit-trails");
expect(serialized).toContain("/notifications/messages");
expect(serialized).toContain("/feature-flags/flags");
expect(serialized).toContain("/onboarding/onboardings");
expect(serialized).toContain("/metrics/summary");
expect(serialized).toContain("/settings/profile");
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: "/", 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) => {
expect(canonicalAdminRouteSlugs).toContain(path.split("/").filter(Boolean)[0] as any);
});
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("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([]);
});
});