Files
kandev/apps/web/components/task-edit-dialog-dependencies.test.tsx
T
2026-09-04 02:35:15 +00:00

146 lines
5.1 KiB
TypeScript

import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@kandev/ui/tooltip";
import type { TaskEditDialogDependenciesState } from "@/hooks/domains/task/use-task-edit-dialog-dependencies";
import { ApiError } from "@/lib/api/client";
import { TaskEditDialogDependencies } from "./task-edit-dialog-dependencies";
const workspaceHydrationMocks = vi.hoisted(() => ({
useWorkspacePRs: vi.fn(),
useWorkspaceMRs: vi.fn(),
}));
vi.mock("@/hooks/domains/github/use-task-pr", () => workspaceHydrationMocks);
vi.mock("@/hooks/domains/gitlab/use-task-mr", () => workspaceHydrationMocks);
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, options?: { count?: number; cycle?: string }) =>
({
"task:dependsOn": "Depends on",
"task:noDependency": "No dependency",
"task:searchTasks": "Search tasks...",
"task:noTasksFound": "No tasks found.",
"tasks:loadingTasks": "Loading tasks...",
"task:dependencyInfoLabel": "About task dependencies",
"task:dependencyInfo": "This task waits until every selected task completes successfully.",
"task:failedToLoadDependencies": "Failed to load dependencies.",
"task:failedToLoadDependencyCandidates": "Failed to load dependency candidates.",
"task:dependencyUpdateFailed": "Failed to update dependencies.",
"task:dependencyCycleError": `Dependency cycle: ${options?.cycle ?? ""}`,
"task:retry": "Retry",
"task:dependencyCount_one": `${options?.count ?? 0} dependency`,
"task:dependencyCount_other": `${options?.count ?? 0} dependencies`,
})[key] ?? key,
}),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
kanban: { tasks: [] },
kanbanMulti: { snapshots: {} },
workspaces: { activeId: null },
workspaceContextGeneration: 0,
taskMRs: { byWorkspaceId: {} },
taskPRs: { byTaskId: {}, workspaceId: null, workspaceContextGeneration: 0 },
}),
}));
function state(
overrides: Partial<TaskEditDialogDependenciesState> = {},
): TaskEditDialogDependenciesState {
return {
taskId: "task-1",
confirmedIds: ["task-2"],
draftIds: ["task-2"],
setDraftIds: vi.fn(),
selectedTitles: { "task-2": "Predecessor" },
candidates: [
{ id: "task-1", title: "Edited task" },
{ id: "task-2", title: "Predecessor" },
{ id: "task-3", title: "Another task" },
{ id: "task-archived", title: "Archived task", isArchived: true },
],
candidatesLoading: false,
query: "",
setQuery: vi.fn(),
loading: false,
loadError: null,
candidateError: null,
saveError: null,
error: null,
submitError: null,
ready: true,
isDirty: false,
save: vi.fn(),
retry: vi.fn(),
retryCandidates: vi.fn(),
...overrides,
};
}
function renderDependencies(value: TaskEditDialogDependenciesState) {
return render(
<TooltipProvider>
<TaskEditDialogDependencies state={value} />
</TooltipProvider>,
);
}
afterEach(cleanup);
describe("TaskEditDialogDependencies", () => {
it("uses the current predecessor set and excludes the edited and archived tasks", () => {
renderDependencies(state());
expect(screen.getByTestId("task-edit-dependencies")).toBeTruthy();
expect(screen.getByText("Depends on")).toBeTruthy();
expect(screen.getByTestId("task-create-dependencies-trigger").textContent).toContain(
"Predecessor",
);
fireEvent.click(screen.getByTestId("task-create-dependencies-trigger"));
const picker = screen.getByTestId("task-create-dependencies-popover");
expect(within(picker).getByTestId("task-create-dependency-option-task-2")).toBeTruthy();
expect(within(picker).getByTestId("task-create-dependency-option-task-3")).toBeTruthy();
expect(within(picker).queryByText("Edited task")).toBeNull();
expect(within(picker).queryByText("Archived task")).toBeNull();
});
it("keeps the editor open and names the cycle when saving fails", () => {
const cycleError = new ApiError("would create a dependency cycle", 409, {
cycle: ["task-1", "task-2", "task-1"],
});
renderDependencies(
state({
saveError: cycleError,
submitError: {
dependencyUpdate: true,
cause: cycleError,
},
error: cycleError,
}),
);
expect(screen.getByTestId("task-edit-dependencies")).toBeTruthy();
expect(screen.getByTestId("task-edit-dependencies-error").textContent).toContain(
"Dependency cycle: task-1 -> task-2 -> task-1",
);
});
it("uses the candidate retry without disabling the editor", () => {
const retryCandidates = vi.fn();
renderDependencies(
state({
candidateError: new Error("candidate search failed"),
retryCandidates,
}),
);
expect(screen.getByTestId("task-create-dependencies-trigger")).toBeTruthy();
fireEvent.click(screen.getByTestId("task-edit-dependencies-candidates-retry"));
expect(retryCandidates).toHaveBeenCalledOnce();
});
});