Files
kandev/apps/web/components/settings/layouts/layout-settings.test.tsx
T
2026-09-22 21:24:43 +01:00

199 lines
6.3 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
finePointer: true,
layoutTab: "profiles" as "profiles" | "sidebar",
useLayoutSettings: vi.fn(),
deleteSelected: vi.fn(),
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, values?: { name?: string }) => (values?.name ? `${key}:${values.name}` : key),
}),
}));
vi.mock("@/hooks/use-responsive-breakpoint", () => ({
useResponsiveBreakpoint: () => ({ isFinePointer: mocks.finePointer }),
}));
vi.mock("@/components/settings/settings-save-provider", () => ({
useSettingsSaveContributor: vi.fn(),
SettingsSaveDirtyScope: ({ children }: { children: (isDirty: boolean) => React.ReactNode }) =>
children(false),
}));
vi.mock("@/components/settings/settings-target", () => ({
SettingsTarget: ({
children,
targetId: _targetId,
...props
}: {
children: React.ReactNode;
targetId?: string;
}) => <div {...props}>{children}</div>,
}));
vi.mock("./layout-editor", () => ({
LayoutEditor: () => <div data-testid="layout-editor" />,
}));
vi.mock("../sidebar-layout-editor", () => ({
SidebarLayoutEditor: ({ embedded }: { embedded?: boolean }) => (
<div data-testid="sidebar-layout-editor-tab" data-embedded={embedded ? "true" : "false"} />
),
}));
vi.mock("./layout-profile-list", () => ({
LayoutProfileList: () => <div data-testid="layout-profile-list" />,
}));
vi.mock("@kandev/ui/tooltip", () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock("./use-layout-settings", () => ({
useLayoutSettings: () => mocks.useLayoutSettings(),
}));
vi.mock("@/hooks/domains/settings/use-settings-tab", () => ({
useSettingsTab: () => ({ value: mocks.layoutTab, selectTab: vi.fn() }),
}));
import { LayoutSettings } from "./layout-settings";
const DELETE_PROFILE_TEST_ID = "layout-profile-delete";
function controller() {
const selectedCustom = {
id: "custom-layout",
name: "Custom layout",
is_default: false,
layout: {},
created_at: "2026-08-20T00:00:00.000Z",
};
return {
profiles: [selectedCustom],
selection: { kind: "custom" as const, id: selectedCustom.id },
selectedCustom,
selectedBuiltIn: null,
selectedBuiltInOverride: null,
compatibility: { status: "editable" as const, profile: selectedCustom, layout: {}, issues: [] },
editorLayout: {},
selectedName: selectedCustom.name,
selectedIsDefault: false,
selectedSavedDefault: false,
defaultActionDisabled: false,
defaultActionLabel: "Use as default",
editorReset: 0,
isDirty: false,
profilesKey: "profiles",
error: null,
save: vi.fn(),
cancel: vi.fn(),
create: vi.fn(),
duplicate: vi.fn(),
setSelection: vi.fn(),
updateSelected: vi.fn(),
updateLayout: vi.fn(),
setDefault: vi.fn(),
resetBuiltIn: vi.fn(),
deleteSelected: mocks.deleteSelected,
};
}
beforeEach(() => {
mocks.finePointer = true;
mocks.layoutTab = "profiles";
mocks.deleteSelected.mockReset();
mocks.useLayoutSettings.mockReturnValue(controller());
});
afterEach(cleanup);
describe("LayoutSettings tabs", () => {
it("lists sidebar customization as a tab and embeds its editor", () => {
render(<LayoutSettings />);
expect(screen.getByRole("tab", { name: "settings:layoutProfiles" })).toBeTruthy();
expect(screen.getByRole("tab", { name: "settings:sidebar" })).toBeTruthy();
cleanup();
mocks.layoutTab = "sidebar";
render(<LayoutSettings />);
expect(screen.getByTestId("sidebar-layout-editor-tab").getAttribute("data-embedded")).toBe(
"true",
);
});
});
describe("LayoutSettings deletion confirmation", () => {
it("anchors confirmation to the selected profile action on fine pointers", async () => {
render(<LayoutSettings />);
const trigger = screen.getByTestId(DELETE_PROFILE_TEST_ID);
fireEvent.click(trigger);
const popover = screen.getByTestId("layout-profile-delete-confirm-popover");
expect(screen.queryByRole("alertdialog")).toBeNull();
fireEvent.click(within(popover).getByRole("button", { name: "settings:cancel" }));
expect(mocks.deleteSelected).not.toHaveBeenCalled();
await waitFor(() => expect(document.activeElement).toBe(trigger));
fireEvent.click(trigger);
fireEvent.click(
within(screen.getByTestId("layout-profile-delete-confirm-popover")).getByTestId(
"layout-profile-delete-confirm",
),
);
await waitFor(() => expect(mocks.deleteSelected).toHaveBeenCalledTimes(1));
});
it("morphs into touch-sized inline actions on coarse pointers", async () => {
mocks.finePointer = false;
render(<LayoutSettings />);
fireEvent.click(screen.getByTestId(DELETE_PROFILE_TEST_ID));
const inline = screen.getByTestId("layout-profile-delete-inline-confirmation");
expect(screen.queryByTestId("layout-profile-delete-confirm-popover")).toBeNull();
expect(within(inline).getByRole("button", { name: "settings:cancel" })).toBeTruthy();
expect(within(inline).getByTestId("layout-profile-delete-confirm").className).toContain(
"min-w-11",
);
fireEvent.click(within(inline).getByRole("button", { name: "settings:cancel" }));
expect(mocks.deleteSelected).not.toHaveBeenCalled();
await waitFor(() =>
expect(document.activeElement).toBe(screen.getByTestId(DELETE_PROFILE_TEST_ID)),
);
fireEvent.click(screen.getByTestId(DELETE_PROFILE_TEST_ID));
fireEvent.click(
within(screen.getByTestId("layout-profile-delete-inline-confirmation")).getByTestId(
"layout-profile-delete-confirm",
),
);
await waitFor(() => expect(mocks.deleteSelected).toHaveBeenCalledTimes(1));
});
it("does not expose deletion for built-in profiles", () => {
mocks.useLayoutSettings.mockReturnValue({
...controller(),
selectedCustom: null,
selection: { kind: "built-in", id: "default" },
selectedName: "Default",
});
render(<LayoutSettings />);
expect(screen.queryByTestId(DELETE_PROFILE_TEST_ID)).toBeNull();
expect(screen.queryByRole("alertdialog")).toBeNull();
});
});