Files
kandev/apps/web/components/kanban-display-dropdown.test.tsx
T
2026-09-14 09:26:24 +01:00

376 lines
14 KiB
TypeScript

import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { KanbanDisplayDropdown } from "./kanban-display-dropdown";
import { TASK_PRIORITY_TOKENS } from "@/lib/tasks/task-priority";
const { useKanbanDisplaySettingsMock } = vi.hoisted(() => ({
useKanbanDisplaySettingsMock: vi.fn(),
}));
vi.mock("@/hooks/use-kanban-display-settings", () => ({
useKanbanDisplaySettings: useKanbanDisplaySettingsMock,
}));
function defaultMockSettings() {
return {
workflows: [],
activeWorkflowId: null,
repositories: [],
repositoriesLoading: false,
allRepositoriesSelected: true,
selectedRepositoryId: null,
enablePreviewOnClick: false,
tasksListShowDetails: false,
eligibleWorkflows: [],
snapshots: {},
hiddenWorkflowStepIds: {},
boardSort: "created_desc" as "created_desc" | "priority_desc",
priorityFilterTokens: [] as string[],
onWorkflowChange: vi.fn(),
onRepositoryChange: vi.fn(),
onTogglePreviewOnClick: vi.fn(),
onToggleTasksListShowDetails: vi.fn(),
onToggleStepVisibility: vi.fn(),
onBoardSortChange: vi.fn(),
onPriorityFilterChange: vi.fn(),
};
}
beforeEach(() => {
useKanbanDisplaySettingsMock.mockReturnValue(defaultMockSettings());
});
afterEach(cleanup);
const TAGS_PLUGIN_ID = "kandev-plugin-tags";
const TAGS_FILTER_ID = "tags";
const TAGS_FILTER_KEY = `${TAGS_PLUGIN_ID}:${TAGS_FILTER_ID}`;
const TAGS_FILTER_TEST_ID = `display-plugin-filter-${TAGS_FILTER_KEY}`;
const PRIORITY_PLUGIN_ID = "kandev-plugin-priority";
const PRIORITY_FILTER_KEY = `${PRIORITY_PLUGIN_ID}:${TAGS_FILTER_ID}`;
const DISPLAY_SETTINGS_PREFIX = "display-settings-";
const DISPLAY_WORKFLOW_FILTER_TEST_ID = "display-workflow-filter";
const DISPLAY_REPOSITORY_FILTER_TEST_ID = "display-repository-filter";
const DISPLAY_BOARD_SORT_TEST_ID = "display-board-sort";
const ARIA_EXPANDED_ATTRIBUTE = "aria-expanded";
const DATA_STATE_ATTRIBUTE = "data-state";
const TAGS_FILTER = {
pluginId: TAGS_PLUGIN_ID,
id: TAGS_FILTER_ID,
label: "Tags",
getOptions: () => [
{ value: "bug", label: "Bug" },
{ value: "feature", label: "Feature" },
],
matches: () => true,
};
function openDropdown() {
const trigger = screen.getByTestId("display-button");
fireEvent.pointerDown(trigger, { button: 0, ctrlKey: false, pointerType: "mouse" });
fireEvent.pointerUp(trigger, { button: 0, ctrlKey: false, pointerType: "mouse" });
fireEvent.click(trigger);
}
function expandGroup(group: "filters" | "sort" | "preview" | "list-rows") {
fireEvent.click(screen.getByTestId(`${DISPLAY_SETTINGS_PREFIX}${group}-toggle`));
}
function groupToggle(group: "filters" | "sort" | "preview" | "list-rows") {
return screen.getByTestId(`${DISPLAY_SETTINGS_PREFIX}${group}-toggle`);
}
describe("KanbanDisplayDropdown — grouped display settings", () => {
it("starts collapsed, expands groups independently, and resets expansion on close", () => {
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
for (const group of ["filters", "sort", "preview"] as const) {
expect(groupToggle(group).getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("false");
}
expect(screen.queryByTestId(DISPLAY_WORKFLOW_FILTER_TEST_ID)).toBeNull();
expect(screen.queryByTestId(DISPLAY_BOARD_SORT_TEST_ID)).toBeNull();
expandGroup("filters");
expect(groupToggle("filters").getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("true");
expect(screen.getByTestId(DISPLAY_WORKFLOW_FILTER_TEST_ID)).not.toBeNull();
expect(groupToggle("sort").getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("false");
expandGroup("sort");
expect(screen.getByTestId(DISPLAY_BOARD_SORT_TEST_ID)).not.toBeNull();
expect(groupToggle("filters").getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("true");
const trigger = screen.getByTestId("display-button");
fireEvent.pointerDown(trigger, { button: 0, ctrlKey: false, pointerType: "mouse" });
fireEvent.pointerUp(trigger, { button: 0, ctrlKey: false, pointerType: "mouse" });
fireEvent.click(trigger);
expect(screen.queryByTestId(`${DISPLAY_SETTINGS_PREFIX}filters-toggle`)).toBeNull();
openDropdown();
expect(groupToggle("filters").getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("false");
});
it("summarizes the current filters, sort, and preview settings", () => {
useKanbanDisplaySettingsMock.mockReturnValue({
...defaultMockSettings(),
workflows: [{ id: "wf-a", name: "Workflow A" }],
activeWorkflowId: "wf-a",
repositories: [{ id: "repo-a", name: "Repository A" }],
allRepositoriesSelected: false,
selectedRepositoryId: "repo-a",
enablePreviewOnClick: true,
boardSort: "priority_desc",
priorityFilterTokens: ["critical"],
});
render(
<KanbanDisplayDropdown
currentPage="kanban"
pluginFilters={[TAGS_FILTER]}
pluginFilterSelections={{ [TAGS_FILTER_KEY]: ["bug"] }}
/>,
);
openDropdown();
expect(screen.getByTestId("display-settings-filters").textContent).toContain(
"Workflow A, Repository A, Critical, Tags: 1 selected",
);
expect(screen.getByTestId("display-settings-sort").textContent).toContain("Priority");
expect(screen.getByTestId("display-settings-preview").textContent).toContain("On");
});
it("does not call preference handlers when groups are toggled", () => {
const settings = defaultMockSettings();
useKanbanDisplaySettingsMock.mockReturnValue(settings);
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
expandGroup("filters");
expandGroup("sort");
expandGroup("preview");
expect(settings.onWorkflowChange).not.toHaveBeenCalled();
expect(settings.onRepositoryChange).not.toHaveBeenCalled();
expect(settings.onBoardSortChange).not.toHaveBeenCalled();
expect(settings.onPriorityFilterChange).not.toHaveBeenCalled();
expect(settings.onTogglePreviewOnClick).not.toHaveBeenCalled();
});
it("keeps page-specific controls inside the filters and list groups", () => {
render(<KanbanDisplayDropdown currentPage="tasks" />);
openDropdown();
expect(groupToggle("filters").getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("false");
expect(groupToggle("list-rows").getAttribute(ARIA_EXPANDED_ATTRIBUTE)).toBe("false");
expect(screen.queryByTestId(`${DISPLAY_SETTINGS_PREFIX}sort-toggle`)).toBeNull();
expect(screen.queryByTestId("display-settings-preview-toggle")).not.toBeNull();
expandGroup("filters");
expect(screen.getByTestId(DISPLAY_WORKFLOW_FILTER_TEST_ID)).not.toBeNull();
expect(screen.getByTestId(DISPLAY_REPOSITORY_FILTER_TEST_ID)).not.toBeNull();
expect(screen.queryByTestId("display-priority-filter-option-critical")).toBeNull();
expandGroup("list-rows");
expect(screen.getByText("Show task details")).not.toBeNull();
});
});
describe("KanbanDisplayDropdown — plugin task filters", () => {
it("renders no plugin filter section when none are registered", () => {
render(<KanbanDisplayDropdown pluginFilters={[]} />);
openDropdown();
expect(screen.queryByTestId(/display-plugin-filter-/)).toBeNull();
});
it("renders a filter section with its options and current selection", () => {
render(
<KanbanDisplayDropdown
pluginFilters={[TAGS_FILTER]}
pluginFilterSelections={{ [TAGS_FILTER_KEY]: ["bug"] }}
/>,
);
openDropdown();
expandGroup("filters");
expect(screen.getByTestId(TAGS_FILTER_TEST_ID)).not.toBeNull();
expect(screen.getByText("Tags")).not.toBeNull();
expect(
screen.getByTestId(`${TAGS_FILTER_TEST_ID}-option-bug`).getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("checked");
expect(
screen
.getByTestId(`${TAGS_FILTER_TEST_ID}-option-feature`)
.getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("unchecked");
});
it("invokes onPluginFilterChange with the updated selection when toggling an option", () => {
const onPluginFilterChange = vi.fn();
render(
<KanbanDisplayDropdown
pluginFilters={[TAGS_FILTER]}
pluginFilterSelections={{}}
onPluginFilterChange={onPluginFilterChange}
/>,
);
openDropdown();
expandGroup("filters");
fireEvent.click(screen.getByTestId(`${TAGS_FILTER_TEST_ID}-option-bug`));
expect(onPluginFilterChange).toHaveBeenCalledWith(TAGS_FILTER_KEY, ["bug"]);
});
});
describe("KanbanDisplayDropdown — plugin task filter identities", () => {
it("keeps same-id filters from different plugins independent", () => {
const onPluginFilterChange = vi.fn();
const firstFilterKey = TAGS_FILTER_KEY;
const secondFilterKey = PRIORITY_FILTER_KEY;
const secondFilter = {
pluginId: PRIORITY_PLUGIN_ID,
id: TAGS_FILTER_ID,
label: "Priority",
getOptions: () => [{ value: "high", label: "High" }],
matches: () => true,
};
render(
<KanbanDisplayDropdown
pluginFilters={[TAGS_FILTER, secondFilter]}
pluginFilterSelections={{ [firstFilterKey]: ["bug"] }}
onPluginFilterChange={onPluginFilterChange}
/>,
);
openDropdown();
expandGroup("filters");
expect(
screen
.getByTestId(`display-plugin-filter-${firstFilterKey}-option-bug`)
.getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("checked");
expect(
screen
.getByTestId(`display-plugin-filter-${secondFilterKey}-option-high`)
.getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("unchecked");
fireEvent.click(screen.getByTestId(`display-plugin-filter-${secondFilterKey}-option-high`));
expect(onPluginFilterChange).toHaveBeenCalledWith(secondFilterKey, ["high"]);
});
it("memoizes options while a filter registration remains stable", () => {
const getOptions = vi.fn(() => [{ value: "bug", label: "Bug" }]);
const filter = {
pluginId: TAGS_PLUGIN_ID,
id: TAGS_FILTER_ID,
label: TAGS_FILTER.label,
getOptions,
matches: () => true,
};
const props = { pluginFilters: [filter] };
const { rerender } = render(<KanbanDisplayDropdown {...props} />);
openDropdown();
expandGroup("filters");
expect(getOptions).toHaveBeenCalledTimes(1);
rerender(<KanbanDisplayDropdown {...props} />);
expect(getOptions).toHaveBeenCalledTimes(1);
});
});
describe("KanbanDisplayDropdown — no Steps section (relocated to the lane)", () => {
it("renders no Steps section, group, or step control on the kanban page", () => {
useKanbanDisplaySettingsMock.mockReturnValue(defaultMockSettings());
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
// Column visibility moved to the swimlane header's ColumnsMenu; the
// dropdown keeps only Workflow, Repository, plugin filters and Preview.
expect(screen.queryByText("kanban:steps")).toBeNull();
expect(screen.queryByTestId(/steps-filter-/)).toBeNull();
expect(screen.queryByTestId(/columns-menu/)).toBeNull();
});
it("renders no step control even when workflows have snapshots with steps", () => {
useKanbanDisplaySettingsMock.mockReturnValue({
...defaultMockSettings(),
eligibleWorkflows: [{ id: "wf-a", name: "Workflow A" }],
snapshots: { "wf-a": { steps: [{ id: "step-1", title: "Step 1", position: 0 }] } },
hiddenWorkflowStepIds: { "wf-a": ["step-1"] },
});
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
expect(screen.queryByTestId("steps-filter-step-step-1")).toBeNull();
expect(screen.queryByTestId("columns-menu-step-step-1")).toBeNull();
});
});
describe("KanbanDisplayDropdown — board sort and priority filter", () => {
it("renders the board sort and priority filter sections on the kanban page", () => {
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
expandGroup("filters");
expandGroup("sort");
expect(screen.getByTestId(DISPLAY_BOARD_SORT_TEST_ID)).not.toBeNull();
TASK_PRIORITY_TOKENS.forEach((token) => {
expect(screen.getByTestId(`display-priority-filter-option-${token}`)).not.toBeNull();
});
});
it("names the board sort select by what it selects, not its current value", () => {
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
expandGroup("sort");
expect(screen.getByRole("combobox", { name: "Board sort" })).not.toBeNull();
});
it("omits the board sort and priority filter sections on the tasks list page", () => {
render(<KanbanDisplayDropdown currentPage="tasks" />);
openDropdown();
expect(screen.queryByTestId(DISPLAY_BOARD_SORT_TEST_ID)).toBeNull();
expect(screen.queryByTestId(/display-priority-filter-option-/)).toBeNull();
});
it("reflects the current priority filter selection", () => {
useKanbanDisplaySettingsMock.mockReturnValue({
...defaultMockSettings(),
priorityFilterTokens: ["critical"],
});
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
expandGroup("filters");
expect(
screen
.getByTestId("display-priority-filter-option-critical")
.getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("checked");
expect(
screen.getByTestId("display-priority-filter-option-high").getAttribute(DATA_STATE_ATTRIBUTE),
).toBe("unchecked");
});
it("invokes onPriorityFilterChange when toggling a priority option", () => {
const onPriorityFilterChange = vi.fn();
useKanbanDisplaySettingsMock.mockReturnValue({
...defaultMockSettings(),
onPriorityFilterChange,
});
render(<KanbanDisplayDropdown currentPage="kanban" />);
openDropdown();
expandGroup("filters");
fireEvent.click(screen.getByTestId("display-priority-filter-option-high"));
expect(onPriorityFilterChange).toHaveBeenCalledWith("high");
});
});