Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/components/ui/commandPalettes/CommandPalette.test.tsx
T

157 lines
6.0 KiB
TypeScript

import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React from "react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { UI_SURFACE_IDS } from "~/application/ui/uiRegistry";
import type { ScopedCommandAction } from "~/services/command-registry/commandRegistry";
import CommandPalette from "./CommandPalette";
type CommandPaletteTestState = {
navigate: ReturnType<typeof vi.fn>;
getCommandRegistryActions: ReturnType<typeof vi.fn>;
useAppOrAdminData: ReturnType<typeof vi.fn>;
useRootData: ReturnType<typeof vi.fn>;
};
declare global {
var __commandPaletteTest: CommandPaletteTestState;
}
globalThis.__commandPaletteTest = {
navigate: vi.fn(),
getCommandRegistryActions: vi.fn(),
useAppOrAdminData: vi.fn(),
useRootData: vi.fn(),
};
globalThis.ResizeObserver =
globalThis.ResizeObserver ??
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
HTMLElement.prototype.scrollIntoView = HTMLElement.prototype.scrollIntoView ?? vi.fn();
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (value: string) => value }),
}));
vi.mock("react-router", async () => {
const actual = await vi.importActual<typeof import("react-router")>("react-router");
return {
...actual,
useFetcher: () => ({ submit: vi.fn() }),
useNavigate: () => globalThis.__commandPaletteTest.navigate,
};
});
vi.mock("~/utils/data/useAppOrAdminData", () => ({
useAppOrAdminData: () => globalThis.__commandPaletteTest.useAppOrAdminData(),
}));
vi.mock("~/utils/data/useRootData", () => ({
useRootData: () => globalThis.__commandPaletteTest.useRootData(),
}));
vi.mock("~/services/command-registry/commandRegistry", () => ({
getCommandRegistryActions: (...args: unknown[]) => globalThis.__commandPaletteTest.getCommandRegistryActions(...args),
}));
const appData = { currentTenant: { slug: "acme" } };
const rootData = { appConfiguration: { app: { features: {} } } };
function renderPalette(props?: Partial<React.ComponentProps<typeof CommandPalette>>) {
return render(
<CommandPalette layout="app" {...props}>
<div>Palette child</div>
</CommandPalette>
);
}
describe("CommandPalette", () => {
// CommandPaletteDialog wird in der Komponente per React.lazy(() => import("./CommandPaletteDialog"))
// geladen. In der vollen Suite kann der KALTE erste dynamische Import unter CPU-Last die
// 1000ms-Default-Frist von findByText reissen (erster Test rot, danach Modul warm -> gruen).
// Vorwaermen macht den ersten Oeffnen-Pfad deterministisch synchron.
beforeAll(async () => {
await import("./CommandPaletteDialog");
});
beforeEach(() => {
const state = globalThis.__commandPaletteTest;
state.navigate.mockReset();
state.getCommandRegistryActions.mockReset();
state.useAppOrAdminData.mockReset();
state.useRootData.mockReset();
state.useAppOrAdminData.mockReturnValue(appData);
state.useRootData.mockReturnValue(rootData);
state.getCommandRegistryActions.mockReturnValue([
{ id: "registry-dashboard", name: "Dashboard", subtitle: "App", perform: vi.fn(), scope: { layout: "app", tenantSlug: "acme" } },
{ id: "registry-denied-looking", name: "Denied Looking", subtitle: "Filtered upstream", perform: vi.fn(), scope: { layout: "app", tenantSlug: "acme", permission: "entity.contacts.view" } },
]);
});
function openPalette() {
fireEvent.keyDown(document, { key: "k", metaKey: true });
}
it("passes the filtered registry-backed actions through for the active layout context", async () => {
renderPalette();
const state = globalThis.__commandPaletteTest;
expect(state.getCommandRegistryActions).not.toHaveBeenCalled();
openPalette();
await screen.findByText("Dashboard", undefined, { timeout: 4000 });
expect(state.getCommandRegistryActions).toHaveBeenCalledWith(expect.objectContaining({
layout: "app",
t: expect.any(Function),
navigate: state.navigate,
appData,
rootData,
}));
expect(screen.getByText("Denied Looking")).toBeInTheDocument();
expect(screen.getByText("Palette child")).toBeInTheDocument();
});
it("uses a custom actions prop instead of registry actions", async () => {
const customActions: ScopedCommandAction[] = [{ id: "custom-action", name: "Custom Action", perform: vi.fn(), scope: { layout: "app" } }];
renderPalette({ actions: customActions });
const state = globalThis.__commandPaletteTest;
expect(state.getCommandRegistryActions).not.toHaveBeenCalled();
openPalette();
expect(await screen.findByText("Custom Action")).toBeInTheDocument();
expect(state.getCommandRegistryActions).not.toHaveBeenCalled();
});
it("renders a safe empty path when app/root data is missing", async () => {
const state = globalThis.__commandPaletteTest;
state.useAppOrAdminData.mockReturnValue(undefined);
state.useRootData.mockReturnValue(undefined);
state.getCommandRegistryActions.mockReturnValue([]);
renderPalette();
openPalette();
await waitFor(() => {
expect(state.getCommandRegistryActions).toHaveBeenCalledWith(expect.objectContaining({ appData: undefined, rootData: undefined }));
});
expect(screen.getByText("Palette child")).toBeInTheDocument();
});
it("renders stable data-ui-id attributes for command palette surfaces and rows", async () => {
renderPalette();
openPalette();
await screen.findByText("Dashboard", undefined, { timeout: 4000 });
expect(document.querySelector(`[data-ui-id="${UI_SURFACE_IDS.commandPalettePortal}"]`)).toBeInTheDocument();
expect(document.querySelector(`[data-ui-id="${UI_SURFACE_IDS.commandPaletteSearch}"]`)).toBeInTheDocument();
expect(document.querySelector(`[data-ui-id="${UI_SURFACE_IDS.commandPaletteResults}"]`)).toBeInTheDocument();
expect(document.querySelectorAll(`[data-ui-id="${UI_SURFACE_IDS.commandPaletteRow}"]`)).toHaveLength(2);
expect(screen.getByText("Dashboard")).toBeInTheDocument();
expect(screen.getByText("Denied Looking")).toBeInTheDocument();
});
});