167 lines
6.3 KiB
TypeScript
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([]);
|
|
});
|
|
});
|