import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it, vi } from "vitest"; import { PERMS } from "@/lib/permission-slugs"; import { anyCapability, fail, type HousekeepingCapabilityContext, ok, } from "../contracts"; import { defaultHousekeepingPreferences } from "../preferences/schema"; import { createHousekeepingRegistry, loadHousekeepingWidgets, } from "../registry"; import { Favorites, movePreferenceId, togglePinnedRoute } from "./favorites"; import { RecentWork } from "./recent-work"; import { WidgetGrid } from "./widget-grid"; import { preferencesAfterSave, toggleOptionalWidget, WidgetSettings, } from "./widget-settings"; const usersView = anyCapability(PERMS.USERS_VIEW); const context: HousekeepingCapabilityContext = { actor: { id: 42, username: "operator", rank: 7 }, isSuperAdmin: false, has: (slug) => slug === PERMS.USERS_VIEW, hasAny: (...slugs) => slugs.includes(PERMS.USERS_VIEW), hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.USERS_VIEW), }; const registry = createHousekeepingRegistry([ { id: "people", labelKey: "people", descriptionKey: "people.description", iconId: "users", canonicalHref: "/ase/people", capability: usersView, routes: [ { id: "people.users", labelKey: "Users", href: "/ase/people/users", capability: usersView, }, { id: "people.tickets", labelKey: "Tickets", href: "/ase/people/tickets", capability: usersView, }, ], searchProviders: [], inboxSources: [], widgets: [ { id: "people.mandatory", owner: "people", capability: usersView, kind: "mandatory", load: async () => ok({ count: 1 }, "mandatory"), }, { id: "people.optional", owner: "people", capability: usersView, kind: "optional", load: async () => ok({ count: 2 }, "optional"), }, { id: "people.failing", owner: "people", capability: usersView, kind: "optional", load: async () => fail("TIMEOUT", "timeout", "failing"), }, ], }, ]); const initialPreferences = { ...defaultHousekeepingPreferences(), pinnedRouteIds: ["people.users"], shortcutOrder: ["people.users"], widgetOrder: ["stale.widget", "people.optional", "people.mandatory"], enabledOptionalWidgetIds: ["people.optional", "people.failing"], }; const save = vi.fn(async (value) => ok(value, "saved")); describe("housekeeping personalization", () => { it("loads enabled widgets independently in reconciled order and reports partial failures", async () => { const result = await loadHousekeepingWidgets( registry, initialPreferences, context, ); expect(result.widgets).toEqual([ { id: "people.optional", data: { count: 2 } }, { id: "people.mandatory", data: { count: 1 } }, ]); expect(result.errors).toEqual([ { widgetId: "people.failing", code: "TIMEOUT" }, ]); }); it("pins, unpins, and reorders only stable preference identifiers", () => { const pinned = togglePinnedRoute(initialPreferences, "people.tickets"); expect(pinned.pinnedRouteIds).toEqual(["people.users", "people.tickets"]); expect(pinned.shortcutOrder).toEqual(["people.users", "people.tickets"]); expect( movePreferenceId(pinned.shortcutOrder, "people.tickets", "people.users"), ).toEqual(["people.tickets", "people.users"]); expect(togglePinnedRoute(pinned, "people.users")).toMatchObject({ pinnedRouteIds: ["people.tickets"], shortcutOrder: ["people.tickets"], }); }); it("locks mandatory widgets, toggles optional widgets, and retains UI state after a failed save", () => { const enabled = toggleOptionalWidget(initialPreferences, "people.optional"); expect(enabled.enabledOptionalWidgetIds).toEqual(["people.failing"]); const failure = fail( "INTERNAL", "errors.housekeeping.preferences.save", "save-failed", ); expect(preferencesAfterSave(initialPreferences, enabled, failure)).toEqual({ preferences: initialPreferences, error: "errors.housekeeping.preferences.save", }); }); it("renders favorites, recent links, locked settings, keyboard ordering, and widget errors accessibly", () => { const widgets = { widgets: [ { id: "people.optional", data: { count: 2 } }, { id: "people.mandatory", data: { count: 1 } }, ], errors: [{ widgetId: "people.failing", code: "TIMEOUT" as const }], }; const routes = registry.domains[0]?.routes ?? []; const definitions = registry.domains[0]?.widgets ?? []; const html = renderToStaticMarkup( <> , ); expect(html).toContain('href="/ase/people/users"'); expect(html).toContain("Required"); expect(html).toContain('disabled=""'); expect(html).toContain("Some widgets failed"); expect(html).not.toContain("TIMEOUT"); const gridSource = readFileSync( resolve( process.cwd(), "src/features/housekeeping/foundation/shell/widget-grid.tsx", ), "utf8", ); expect(gridSource).toContain("KeyboardSensor"); expect(gridSource).toContain("sortableKeyboardCoordinates"); }); });