f8708da1e0
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
217 lines
7.7 KiB
TypeScript
217 lines
7.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { pluginRegistry } from "@/lib/plugins/registry";
|
|
import { buildTaskActionsMenuEntries } from "./task-actions-menu-entries";
|
|
import {
|
|
buildCardPluginEntries,
|
|
type KanbanCardMenuEntry,
|
|
} from "@/components/kanban-card-menu-items";
|
|
|
|
const PLUGIN_ID = "test-task-actions-menu-plugin";
|
|
const REMOVE_SEPARATOR = "remove-separator";
|
|
|
|
function itemKeys(entries: KanbanCardMenuEntry[]): string[] {
|
|
return entries.map((entry) => entry.key);
|
|
}
|
|
|
|
const baseArgs = {
|
|
currentWorkflowId: "workflow-1",
|
|
currentStepId: "step-1",
|
|
workflows: [
|
|
{ id: "workflow-1", name: "Workflow 1" },
|
|
{ id: "workflow-2", name: "Workflow 2" },
|
|
],
|
|
stepsByWorkflowId: {
|
|
"workflow-1": [
|
|
{ id: "step-1", title: "Step 1" },
|
|
{ id: "step-2", title: "Step 2" },
|
|
],
|
|
"workflow-2": [{ id: "step-3", title: "Step 3" }],
|
|
},
|
|
onEdit: vi.fn(),
|
|
onArchive: vi.fn(),
|
|
onDelete: vi.fn(),
|
|
onDetach: vi.fn(),
|
|
parentTaskId: "parent-1",
|
|
currentPriority: "high",
|
|
onSelectPriority: vi.fn(),
|
|
onMoveToStep: vi.fn(),
|
|
onSendToWorkflow: vi.fn(),
|
|
// A real card always wires at least one link handler; omitting it here
|
|
// would silently drop "link" from the entries array and let the order
|
|
// assertion below pass without ever exercising its actual position.
|
|
onLinkPullRequest: vi.fn(),
|
|
};
|
|
|
|
describe("buildTaskActionsMenuEntries — prebuilt plugin entries", () => {
|
|
afterEach(() => pluginRegistry.unregisterPlugin(PLUGIN_ID));
|
|
|
|
// Regression: these tiers force `forceFlatEdit`, so a caller's prebuilt set --
|
|
// built for the card, where an edit-group action nests -- would render the very
|
|
// Edit submenu the tier exists to suppress.
|
|
it("ignores prebuilt entries, keeping the flat Edit item", () => {
|
|
pluginRegistry.forPlugin(PLUGIN_ID).registerTaskMenuAction({
|
|
id: "nested",
|
|
label: "Nested",
|
|
group: "edit",
|
|
items: () => [{ id: "child", label: "Child", run: vi.fn() }],
|
|
run: vi.fn(),
|
|
});
|
|
const context = {
|
|
workspaceId: "ws-1",
|
|
taskId: "task-1",
|
|
taskTitle: "Task",
|
|
workflowStepId: "step-1",
|
|
presentation: "desktop" as const,
|
|
};
|
|
const cardEntries = buildCardPluginEntries({ onEdit: vi.fn(), pluginMenuContext: context });
|
|
|
|
const entries = buildTaskActionsMenuEntries("normal", {
|
|
...baseArgs,
|
|
pluginEntries: cardEntries,
|
|
});
|
|
|
|
expect(entries.find((entry) => entry.key === "edit")?.kind).toBe("item");
|
|
});
|
|
});
|
|
|
|
describe("buildTaskActionsMenuEntries — normal tier", () => {
|
|
afterEach(() => pluginRegistry.unregisterPlugin(PLUGIN_ID));
|
|
|
|
it("always uses the flat Edit item, even when a plugin edit-group action is registered", () => {
|
|
pluginRegistry.forPlugin(PLUGIN_ID).registerTaskMenuAction({
|
|
id: "edit-action",
|
|
group: "edit",
|
|
label: "Plugin edit action",
|
|
run: vi.fn(),
|
|
});
|
|
|
|
const entries = buildTaskActionsMenuEntries("normal", baseArgs);
|
|
const edit = entries.find((entry) => entry.key === "edit");
|
|
|
|
expect(edit?.kind).toBe("item");
|
|
});
|
|
|
|
it("presents the full card order for a resolved, unarchived subject", () => {
|
|
const entries = buildTaskActionsMenuEntries("normal", baseArgs);
|
|
expect(itemKeys(entries)).toEqual([
|
|
"priority",
|
|
"edit-separator",
|
|
"edit",
|
|
"relationships-separator",
|
|
"link",
|
|
"detach",
|
|
"move-separator",
|
|
"move-to",
|
|
"send-to-workflow",
|
|
REMOVE_SEPARATOR,
|
|
"archive",
|
|
"delete",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("buildTaskActionsMenuEntries — in-flight disabling (AC-TASKS-TASK-ACTIONS-MENU-004.1)", () => {
|
|
function itemByKey(entries: KanbanCardMenuEntry[], key: string) {
|
|
return entries.find((entry) => entry.key === key);
|
|
}
|
|
|
|
it("disables Archive, Detach, and Delete while an archive request from this menu is in flight", () => {
|
|
const entries = buildTaskActionsMenuEntries("normal", { ...baseArgs, isArchiving: true });
|
|
|
|
expect(itemByKey(entries, "archive")).toMatchObject({ disabled: true });
|
|
expect(itemByKey(entries, "detach")).toMatchObject({ disabled: true });
|
|
expect(itemByKey(entries, "delete")).toMatchObject({ disabled: true });
|
|
});
|
|
|
|
it("disables Archive, Detach, and Delete while a delete request from this menu is in flight", () => {
|
|
const entries = buildTaskActionsMenuEntries("normal", { ...baseArgs, isDeleting: true });
|
|
|
|
expect(itemByKey(entries, "archive")).toMatchObject({ disabled: true });
|
|
expect(itemByKey(entries, "detach")).toMatchObject({ disabled: true });
|
|
expect(itemByKey(entries, "delete")).toMatchObject({ disabled: true });
|
|
});
|
|
|
|
it("disables Archive, Detach, and Delete while a detach request from this menu is in flight", () => {
|
|
const entries = buildTaskActionsMenuEntries("normal", { ...baseArgs, isDetaching: true });
|
|
|
|
expect(itemByKey(entries, "archive")).toMatchObject({ disabled: true });
|
|
expect(itemByKey(entries, "detach")).toMatchObject({ disabled: true });
|
|
expect(itemByKey(entries, "delete")).toMatchObject({ disabled: true });
|
|
});
|
|
|
|
it("leaves every entry enabled when nothing from this menu is in flight", () => {
|
|
const entries = buildTaskActionsMenuEntries("normal", baseArgs);
|
|
|
|
expect(itemByKey(entries, "archive")).toMatchObject({ disabled: false });
|
|
expect(itemByKey(entries, "detach")).toMatchObject({ disabled: false });
|
|
expect(itemByKey(entries, "delete")).toMatchObject({ disabled: false });
|
|
});
|
|
});
|
|
|
|
describe("buildTaskActionsMenuEntries — archived tier", () => {
|
|
afterEach(() => pluginRegistry.unregisterPlugin(PLUGIN_ID));
|
|
|
|
it("presents Delete alone with no leading separator when no plugin action is admitted", () => {
|
|
const entries = buildTaskActionsMenuEntries("archived", baseArgs);
|
|
expect(itemKeys(entries)).toEqual(["delete"]);
|
|
});
|
|
|
|
it("presents admitted plugin primary actions, a separator, then Delete", () => {
|
|
pluginRegistry.forPlugin(PLUGIN_ID).registerTaskMenuAction({
|
|
id: "primary-action",
|
|
group: "primary",
|
|
label: "Plugin primary action",
|
|
run: vi.fn(),
|
|
});
|
|
|
|
const entries = buildTaskActionsMenuEntries("archived", baseArgs);
|
|
expect(itemKeys(entries)).toEqual([
|
|
`plugin-primary-${PLUGIN_ID}:primary-action`,
|
|
REMOVE_SEPARATOR,
|
|
"delete",
|
|
]);
|
|
});
|
|
|
|
it("omits Edit, Move to, Send to workflow, Link, Detach from parent, and Archive", () => {
|
|
const entries = buildTaskActionsMenuEntries("archived", baseArgs);
|
|
const keys = itemKeys(entries);
|
|
for (const omitted of ["edit", "move-to", "send-to-workflow", "link", "archive", "detach"]) {
|
|
expect(keys).not.toContain(omitted);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("buildTaskActionsMenuEntries — unresolved board row tier", () => {
|
|
afterEach(() => pluginRegistry.unregisterPlugin(PLUGIN_ID));
|
|
|
|
it("presents only Archive and Delete when no plugin action is admitted", () => {
|
|
const entries = buildTaskActionsMenuEntries("unresolved-row", baseArgs);
|
|
expect(itemKeys(entries)).toEqual(["archive", "delete"]);
|
|
});
|
|
|
|
it("orders admitted plugin primary actions before Archive", () => {
|
|
pluginRegistry.forPlugin(PLUGIN_ID).registerTaskMenuAction({
|
|
id: "primary-action",
|
|
group: "primary",
|
|
label: "Plugin primary action",
|
|
run: vi.fn(),
|
|
});
|
|
|
|
const entries = buildTaskActionsMenuEntries("unresolved-row", baseArgs);
|
|
expect(itemKeys(entries)).toEqual([
|
|
`plugin-primary-${PLUGIN_ID}:primary-action`,
|
|
REMOVE_SEPARATOR,
|
|
"archive",
|
|
"delete",
|
|
]);
|
|
});
|
|
|
|
it("omits Edit, Move to, Send to workflow, Link, and Detach from parent even with a parentTaskId", () => {
|
|
const entries = buildTaskActionsMenuEntries("unresolved-row", baseArgs);
|
|
const keys = itemKeys(entries);
|
|
for (const omitted of ["edit", "move-to", "send-to-workflow", "link", "detach"]) {
|
|
expect(keys).not.toContain(omitted);
|
|
}
|
|
});
|
|
});
|