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

232 lines
8.9 KiB
TypeScript

import { existsSync } from "node:fs";
import { vi, describe, it, expect } from "vitest";
import { AppSidebar } from "./AppSidebar";
import { UI_NAV_IDS } from "~/application/ui/uiRegistry";
import { EntitySimple } from "~/utils/db/entities/entities.db.server";
import { resolveRepoPath } from "~/utils/testing/repoPaths";
describe("AppSidebar", () => {
const starterProductAccess = {
"crm-basic": { allowed: true },
"email-marketing": { allowed: false },
api: { allowed: false },
"workflow-engine": { allowed: false },
"ai-credits": { allowed: true },
};
const proProductAccess = {
"crm-basic": { allowed: true },
"email-marketing": { allowed: true },
api: { allowed: true },
"workflow-engine": { allowed: false },
"ai-credits": { allowed: true },
};
const businessProductAccess = {
"crm-basic": { allowed: true },
"email-marketing": { allowed: true },
api: { allowed: true },
"workflow-engine": { allowed: true },
"ai-credits": { allowed: true },
};
it("returns the expected sidebar items", () => {
const t = vi.fn().mockImplementation((v: string) => v) as any;
const i18n = { exists: vi.fn().mockReturnValue(false) } as any;
const entities = [
{ name: "entity1", slug: "entity1", titlePlural: "Entity 1", showInSidebar: true, icon: "" },
{ name: "entity2", slug: "entity2", titlePlural: "Entity 2", showInSidebar: false, icon: "" },
{ name: "entity3", slug: "entity3", titlePlural: "Entity 3", showInSidebar: true, icon: "" },
] as EntitySimple[];
const result = AppSidebar({ t, i18n, tenantId: "test-user", entities, entityGroups: [], appConfiguration: null });
const serialized = JSON.stringify(result);
expect(result[0]).toMatchObject({
title: "",
path: "",
items: [
expect.objectContaining({
id: "ui.nav.app.dashboard",
title: "app.sidebar.dashboard",
path: "/test-user/dashboard",
}),
expect.objectContaining({
id: UI_NAV_IDS.appNotifications,
title: "app.sidebar.notifications",
path: "/test-user/notifications",
}),
expect.objectContaining({
id: "ui.nav.app.entity.entity1",
title: "Entity 1",
path: "/test-user/entity1",
permission: "entity.entity1.view",
}),
expect.objectContaining({
id: "ui.nav.app.entity.entity3",
title: "Entity 3",
path: "/test-user/entity3",
permission: "entity.entity3.view",
}),
],
});
expect(result[1]).toMatchObject({
title: "",
path: "",
isSecondary: true,
items: [
expect.objectContaining({
id: "ui.nav.app.settings",
title: "app.sidebar.settings",
path: "/test-user/settings",
redirectTo: "/test-user/settings/profile",
}),
expect.objectContaining({
id: "ui.nav.app.switchToAdmin",
title: "admin.switchToAdmin",
path: "/dashboard",
adminOnly: true,
}),
],
});
expect(serialized).not.toContain("/app/");
expect(serialized).not.toContain("/admin/");
expect(serialized).not.toContain("/blog");
expect(serialized).not.toContain("/pricing");
expect(serialized).not.toContain("/email-marketing");
expect(serialized).not.toContain("/workflow-engine");
expect(serialized).not.toContain("/widgets");
expect(serialized).not.toContain("/portals");
expect(serialized).not.toContain("/sites/");
});
it("renders plan-backed core feature links from product access decisions", () => {
const t = vi.fn().mockImplementation((v: string) => v) as any;
const i18n = { exists: vi.fn().mockReturnValue(false) } as any;
const starter = JSON.stringify(
AppSidebar({ t, i18n, tenantId: "starter", entities: [], entityGroups: [], appConfiguration: null, productAccess: starterProductAccess })
);
const pro = JSON.stringify(
AppSidebar({ t, i18n, tenantId: "pro", entities: [], entityGroups: [], appConfiguration: null, productAccess: proProductAccess })
);
const business = JSON.stringify(
AppSidebar({ t, i18n, tenantId: "business", entities: [], entityGroups: [], appConfiguration: null, productAccess: businessProductAccess })
);
expect(starter).toContain("/starter/crm");
expect(starter).toContain("/starter/settings/credits");
expect(starter).not.toContain("/starter/email-marketing");
expect(starter).not.toContain("/starter/settings/api");
expect(starter).not.toContain("/starter/workflow-engine");
expect(pro).toContain("/pro/crm");
expect(pro).toContain("/pro/email-marketing");
expect(pro).toContain("/pro/settings/api");
expect(pro).not.toContain("/pro/workflow-engine");
expect(business).toContain("/business/crm");
expect(business).toContain("/business/email-marketing");
expect(business).toContain("/business/settings/api");
expect(business).toContain("/business/workflow-engine");
});
it("marks collapsible entity groups as expand-only parents", () => {
const t = vi.fn().mockImplementation((v: string) => v) as any;
const i18n = { exists: vi.fn().mockReturnValue(false) } as any;
const entities = [
{ name: "company", slug: "companies", titlePlural: "Companies", showInSidebar: true, icon: "" },
] as EntitySimple[];
const result = AppSidebar({
t,
i18n,
tenantId: "acme",
entities: [],
entityGroups: [
{
slug: "sales",
title: "Sales",
icon: "",
collapsible: true,
section: null,
entities: [{ entity: entities[0] }],
},
] as any,
appConfiguration: null,
});
const group = result[0].items?.find((item) => item.id === "ui.nav.app.entityGroup.sales");
expect(group).toMatchObject({
isCollapsible: true,
path: "/acme/g/sales",
});
expect(group?.items?.[0]).toMatchObject({ title: "shared.overview", path: "/acme/g/sales" });
});
it("translates DB-backed labels only when locale keys exist", () => {
const t = vi.fn().mockImplementation((v: string) => `translated:${v}`) as any;
const i18n = { exists: vi.fn().mockImplementation((value: string) => value === "models.contact.plural" || value === "groups.sales") } as any;
const entities = [
{ name: "contact", slug: "contacts", titlePlural: "models.contact.plural", showInSidebar: true, icon: "" },
{ name: "custom", slug: "custom", titlePlural: "Custom records", showInSidebar: true, icon: "" },
] as EntitySimple[];
const result = AppSidebar({
t,
i18n,
tenantId: "acme",
entities,
entityGroups: [
{
slug: "sales",
title: "groups.sales",
icon: "",
collapsible: true,
section: null,
entities: [{ entity: entities[0] }],
},
] as any,
appConfiguration: null,
});
const serialized = JSON.stringify(result);
expect(serialized).toContain("translated:models.contact.plural");
expect(serialized).toContain("translated:groups.sales");
expect(serialized).toContain("Custom records");
expect(serialized).not.toContain('"models.contact.plural"');
expect(serialized).not.toContain('"groups.sales"');
});
it("keeps static tenant sidebar targets backed by registered route files", () => {
const t = vi.fn().mockImplementation((v: string) => v) as any;
const i18n = { exists: vi.fn().mockReturnValue(false) } as any;
const result = AppSidebar({ t, i18n, tenantId: "acme", entities: [], entityGroups: [], appConfiguration: null, productAccess: businessProductAccess });
const serialized = JSON.stringify(result);
expect(serialized).toContain("/acme/dashboard");
expect(serialized).toContain("/acme/notifications");
expect(serialized).toContain("/acme/crm");
expect(serialized).toContain("/acme/email-marketing");
expect(serialized).toContain("/acme/workflow-engine");
expect(serialized).toContain("/acme/settings/api");
expect(serialized).toContain("/acme/settings/credits");
expect(serialized).toContain("/acme/settings");
expect(serialized).toContain("/acme/settings/profile");
expect(serialized).not.toContain("/app/");
expect(serialized).not.toContain("/admin/");
[
"apps/web/app/routes/app.$tenant/dashboard.tsx",
"apps/web/app/routes/app.$tenant/notifications.tsx",
"apps/web/app/routes/app.$tenant/crm.tsx",
"apps/web/app/routes/app.$tenant/email-marketing.tsx",
"apps/web/app/routes/app.$tenant/__workflows/workflow-engine.tsx",
"apps/web/app/routes/app.$tenant/settings/api/index.tsx",
"apps/web/app/routes/app.$tenant/settings/credits.tsx",
"apps/web/app/routes/app.$tenant/settings.tsx",
"apps/web/app/routes/app.$tenant/settings/profile.tsx",
].forEach((routeFile) => {
expect(existsSync(resolveRepoPath(routeFile))).toBe(true);
});
});
});