157 lines
6.0 KiB
TypeScript
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();
|
|
});
|
|
});
|