diff --git a/src/features/housekeeping/foundation/localization-contract.test.ts b/src/features/housekeeping/foundation/localization-contract.test.ts index 76aadf47..ab8ea6b4 100644 --- a/src/features/housekeeping/foundation/localization-contract.test.ts +++ b/src/features/housekeeping/foundation/localization-contract.test.ts @@ -21,6 +21,39 @@ const requiredKeys = [ "pages.housekeeping.states.error.description", ]; +const expectedDomainMessages = [ + { + id: "operations", + labelKey: "pages.housekeeping.domains.operations.title", + descriptionKey: "pages.housekeeping.domains.operations.description", + }, + { + id: "people", + labelKey: "pages.housekeeping.domains.people.title", + descriptionKey: "pages.housekeeping.domains.people.description", + }, + { + id: "content", + labelKey: "pages.housekeeping.domains.content.title", + descriptionKey: "pages.housekeeping.domains.content.description", + }, + { + id: "economy", + labelKey: "pages.housekeeping.domains.economy.title", + descriptionKey: "pages.housekeeping.domains.economy.description", + }, + { + id: "hotel", + labelKey: "pages.housekeeping.domains.hotel.title", + descriptionKey: "pages.housekeeping.domains.hotel.description", + }, + { + id: "system", + labelKey: "pages.housekeeping.domains.system.title", + descriptionKey: "pages.housekeeping.domains.system.description", + }, +] as const; + function resolveMessage(messages: unknown, key: string): unknown { return key.split(".").reduce((value, segment) => { if (value === null || typeof value !== "object") return undefined; @@ -44,14 +77,34 @@ describe("housekeeping localization contract", () => { "states", ]); - for (const key of [ - ...HOUSEKEEPING_MANIFESTS.flatMap((manifest) => [ - manifest.labelKey, - manifest.descriptionKey, - ]), - ...requiredKeys, - ]) { + for (const key of requiredKeys) { expect(resolveMessage(messages, key), key).toEqual(expect.any(String)); } + + for (const [index, expected] of expectedDomainMessages.entries()) { + const manifest = HOUSEKEEPING_MANIFESTS[index]; + + expect(manifest).toMatchObject(expected); + expect( + resolveMessage(messages, expected.labelKey), + expected.labelKey, + ).toEqual(expect.any(String)); + expect( + resolveMessage(messages, expected.descriptionKey), + expected.descriptionKey, + ).toEqual(expect.any(String)); + } + }); + + it("uses idiomatic Italian copy for the housekeeping shell", () => { + expect(resolveMessage(itMessages, "pages.housekeeping.preview.badge")).toBe( + "Anteprima della struttura", + ); + expect( + resolveMessage(itMessages, "pages.housekeeping.navigation.primary"), + ).toBe("Aree housekeeping"); + expect( + resolveMessage(itMessages, "pages.housekeeping.states.loading.title"), + ).toBe("Caricamento in corso"); }); }); diff --git a/src/features/housekeeping/foundation/navigation.test.ts b/src/features/housekeeping/foundation/navigation.test.ts index 1e62343b..82ae3c0d 100644 --- a/src/features/housekeeping/foundation/navigation.test.ts +++ b/src/features/housekeeping/foundation/navigation.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { PERMS } from "@/lib/permission-slugs"; +import { HOUSEKEEPING_MANIFESTS } from "../manifests"; import { anyCapability, type HousekeepingCapabilityContext } from "./contracts"; import { buildHousekeepingNavigation } from "./navigation"; import { createHousekeepingRegistry } from "./registry"; @@ -117,4 +118,34 @@ describe("housekeeping navigation", () => { expect(translate).not.toHaveBeenCalledWith("system.title"); expect(translate).not.toHaveBeenCalledWith("system.description"); }); + it("shows the real People manifest only for its view capabilities", () => { + const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS); + const translate = (key: string) => key; + + expect( + buildHousekeepingNavigation( + registry, + context([PERMS.MOD_CFH_VIEW]), + translate, + ).map((domain) => domain.id), + ).toContain("people"); + expect( + buildHousekeepingNavigation( + registry, + context([PERMS.MOD_CFH_VIEW]), + translate, + ).map((domain) => domain.id), + ).not.toContain("economy"); + + for (const slug of [PERMS.MOD_ACTIONS, PERMS.MOD_CFH_EDIT]) { + const visibleDomainIds = buildHousekeepingNavigation( + registry, + context([slug]), + translate, + ).map((domain) => domain.id); + + expect(visibleDomainIds).not.toContain("people"); + expect(visibleDomainIds).not.toContain("economy"); + } + }); }); diff --git a/src/features/housekeeping/foundation/registry.test.ts b/src/features/housekeeping/foundation/registry.test.ts index e5d57cbc..97c3d07d 100644 --- a/src/features/housekeeping/foundation/registry.test.ts +++ b/src/features/housekeeping/foundation/registry.test.ts @@ -21,6 +21,82 @@ const manifest = ( routes: [], }); +const expectedManifests = [ + { + id: "operations", + iconId: "inbox", + previewHref: "/admin-next/operations", + labelKey: "pages.housekeeping.domains.operations.title", + descriptionKey: "pages.housekeeping.domains.operations.description", + slugs: [PERMS.ADMIN_DASHBOARD], + }, + { + id: "people", + iconId: "users", + previewHref: "/admin-next/people", + labelKey: "pages.housekeeping.domains.people.title", + descriptionKey: "pages.housekeeping.domains.people.description", + slugs: [ + PERMS.USERS_VIEW, + PERMS.MODERATION_VIEW, + PERMS.TICKETS_VIEW, + PERMS.BANS_VIEW, + PERMS.MOD_DASHBOARD, + PERMS.MOD_CFH_VIEW, + PERMS.MOD_TEAM_VIEW, + PERMS.MOD_TICKETS_VIEW, + PERMS.MOD_USERS_VIEW, + PERMS.MOD_BANS_VIEW, + ], + }, + { + id: "content", + iconId: "file-text", + previewHref: "/admin-next/content", + labelKey: "pages.housekeeping.domains.content.title", + descriptionKey: "pages.housekeeping.domains.content.description", + slugs: [ + PERMS.NEWS_VIEW, + PERMS.PAGES_VIEW, + PERMS.BANNERS_VIEW, + PERMS.EVENTS_VIEW, + PERMS.POLLS_VIEW, + PERMS.PREFIXES_VIEW, + ], + }, + { + id: "economy", + iconId: "gem", + previewHref: "/admin-next/economy", + labelKey: "pages.housekeeping.domains.economy.title", + descriptionKey: "pages.housekeeping.domains.economy.description", + slugs: [PERMS.CATALOG_VIEW, PERMS.SHOP_VIEW], + }, + { + id: "hotel", + iconId: "hotel", + previewHref: "/admin-next/hotel", + labelKey: "pages.housekeeping.domains.hotel.title", + descriptionKey: "pages.housekeeping.domains.hotel.description", + slugs: [PERMS.ROOMS_VIEW, PERMS.RADIO_VIEW, PERMS.ASSETS_IMPORT], + }, + { + id: "system", + iconId: "settings", + previewHref: "/admin-next/system", + labelKey: "pages.housekeeping.domains.system.title", + descriptionKey: "pages.housekeeping.domains.system.description", + slugs: [ + PERMS.SETTINGS_VIEW, + PERMS.LOGS_VIEW, + PERMS.ANALYTICS_VIEW, + PERMS.DEVOPS_VIEW, + PERMS.NOTIFICATIONS_VIEW, + PERMS.PERMISSIONS_MANAGE, + PERMS.RCON_EXECUTE, + ], + }, +] as const; describe("housekeeping registry", () => { it("registers the six approved domains in their locked order", () => { const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS); @@ -150,4 +226,72 @@ describe("housekeeping registry", () => { ]), ).toThrow("unknown capability slug: admin.ghost.route"); }); + it("locks every real manifest to its approved presentation and capability group", () => { + expect(HOUSEKEEPING_MANIFESTS).toHaveLength(6); + + for (const [index, expected] of expectedManifests.entries()) { + const actual = HOUSEKEEPING_MANIFESTS[index]; + + expect(actual).toMatchObject({ + id: expected.id, + iconId: expected.iconId, + previewHref: expected.previewHref, + labelKey: expected.labelKey, + descriptionKey: expected.descriptionKey, + }); + expect(actual.routes).toEqual([]); + expect(actual.capability).toEqual({ mode: "any", slugs: expected.slugs }); + } + }); + + it("rejects empty domain and route capability requirements", () => { + expect(() => + createHousekeepingRegistry([ + { ...manifest("operations"), capability: { mode: "any", slugs: [] } }, + ]), + ).toThrow("empty capability requirement"); + expect(() => + createHousekeepingRegistry([ + { + ...manifest("people"), + routes: [ + { + id: "users", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/people/users", + capability: { mode: "any", slugs: [] }, + }, + ], + }, + ]), + ).toThrow("empty capability requirement"); + }); + + it("rejects duplicate route identities and hrefs across domains", () => { + const route = { + id: "shared", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/shared", + capability: anyCapability(PERMS.USERS_VIEW), + }; + + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), routes: [route] }, + { + ...manifest("content"), + routes: [{ ...route, href: "/admin-next/content/shared" }], + }, + ]), + ).toThrow("duplicate route id"); + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), routes: [route] }, + { + ...manifest("content"), + routes: [{ ...route, id: "content-shared" }], + }, + ]), + ).toThrow("duplicate route href"); + }); }); diff --git a/src/messages/it.json b/src/messages/it.json index 758f9b1b..01765936 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -3263,13 +3263,13 @@ }, "housekeeping": { "preview": { - "badge": "Anteprima della base", + "badge": "Anteprima della struttura", "commandDisabled": "Ricerca e comandi saranno abilitati in un sottoprogetto successivo.", "backToSite": "Torna al sito" }, "navigation": { "skipToContent": "Vai al contenuto", - "primary": "Domini housekeeping", + "primary": "Aree housekeeping", "contextual": "Navigazione del dominio" }, "domains": { @@ -3300,8 +3300,8 @@ }, "states": { "loading": { - "title": "Caricamento housekeeping", - "description": "Preparazione degli strumenti housekeeping disponibili." + "title": "Caricamento in corso", + "description": "Stiamo preparando gli strumenti housekeeping disponibili." }, "empty": { "title": "Nessun contenuto disponibile",