Files
kandev/apps/web/components/github/my-github/quick-task-launcher.test.tsx
T
2026-09-21 18:22:29 +01:00

310 lines
8.6 KiB
TypeScript

import { render, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { Icon } from "@tabler/icons-react";
import type { GitHubIssue, GitHubPR } from "@/lib/types/github";
import {
repositoryId,
workspaceId,
type Repository,
type Task,
type Workflow,
type WorkflowStep,
} from "@/lib/types/http";
import { QuickTaskLauncher, type LaunchPayload, type TaskPreset } from "./quick-task-launcher";
const NOW = "2026-07-01T00:00:00Z";
const WORKSPACE_ID = "workspace-1";
const WORKFLOW_ID = "workflow-1";
const PR_HEAD_BRANCH = "feature/adding-a-download-ot-5sl";
const TASK_WORKTREE_ROOT = "/root/.kandev/tasks";
const TASK_WORKTREE_PATH = "/root/.kandev/tasks/pr-1541-fix-skip-cle_3bm/kdlbs-kandev";
const REPO_URL = "https://github.com/kdlbs/kandev/pull/1567";
const ISSUE_URL = "https://github.com/kdlbs/kandev/issues/1567";
const LOCAL_REPO_ID = "local-repo";
const mocks = vi.hoisted(() => ({
dialogProps: undefined as
| {
initialValues?: Record<string, unknown>;
onSuccess?: (task: Task, mode?: "create" | "edit", meta?: { autoFocus?: boolean }) => void;
}
| undefined,
push: vi.fn(),
createTaskPR: vi.fn(),
linkTaskIssue: vi.fn(),
upsertTaskIssue: vi.fn(),
}));
vi.mock("@/components/task-create-dialog", () => ({
TaskCreateDialog: (props: { initialValues?: Record<string, unknown> }) => {
mocks.dialogProps = props;
return null;
},
}));
vi.mock("@/lib/routing/client-router", () => ({
useRouter: () => ({ push: mocks.push }),
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: typeof mocks) => unknown) => selector(mocks),
}));
vi.mock("@/lib/api/domains/github-api", () => ({
createTaskPR: mocks.createTaskPR,
linkTaskIssue: mocks.linkTaskIssue,
}));
const preset: TaskPreset = {
id: "review",
label: "Review",
hint: "Review the PR",
icon: (() => null) as unknown as Icon,
prompt: ({ url }) => `Review ${url}`,
};
const workflow = {
id: WORKFLOW_ID,
workspace_id: WORKSPACE_ID,
name: "Workflow",
created_at: NOW,
updated_at: NOW,
} as Workflow;
const step = {
id: "step-1",
workflow_id: WORKFLOW_ID,
name: "Backlog",
position: 1,
color: "gray",
created_at: NOW,
updated_at: NOW,
} as WorkflowStep;
function pr(overrides: Partial<GitHubPR> = {}): GitHubPR {
return {
number: 1567,
title: "feat: add Download option",
url: REPO_URL,
html_url: REPO_URL,
state: "open",
head_branch: PR_HEAD_BRANCH,
base_branch: "main",
author_login: "contributor",
repo_owner: "kdlbs",
repo_name: "kandev",
draft: false,
mergeable: true,
additions: 0,
deletions: 0,
requested_reviewers: [],
created_at: NOW,
updated_at: NOW,
merged_at: null,
closed_at: null,
...overrides,
};
}
function issue(overrides: Partial<GitHubIssue> = {}): GitHubIssue {
return {
number: 1567,
title: "add Download option",
body: "",
url: ISSUE_URL,
html_url: ISSUE_URL,
state: "open",
author_login: "contributor",
repo_owner: "kdlbs",
repo_name: "kandev",
labels: [],
assignees: [],
created_at: NOW,
updated_at: NOW,
closed_at: null,
...overrides,
};
}
type RepoOverrides = Omit<Partial<Repository>, "id" | "workspace_id"> & { id: string };
function repo(overrides: RepoOverrides): Repository {
const { id, ...rest } = overrides;
return {
id: repositoryId(id),
workspace_id: workspaceId(WORKSPACE_ID),
name: "kdlbs/kandev",
source_type: "local",
local_path: "/work/kandev",
provider: "github",
provider_repo_id: "",
provider_owner: "kdlbs",
provider_name: "kandev",
default_branch: "main",
worktree_branch_prefix: "feature/",
pull_before_worktree: true,
setup_script: "",
cleanup_script: "",
dev_script: "",
copy_files: "",
created_at: NOW,
updated_at: NOW,
...rest,
};
}
function renderLauncher(repositories: Repository[], prOverrides: Partial<GitHubPR> = {}) {
const payload: LaunchPayload = { kind: "pr", pr: pr(prOverrides), preset };
render(
<QuickTaskLauncher
workspaceId={WORKSPACE_ID}
workflows={[workflow]}
steps={[step]}
repositories={repositories}
payload={payload}
onClose={vi.fn()}
/>,
);
return mocks.dialogProps?.initialValues;
}
function renderIssueLauncher(
repositories: Repository[],
issueOverrides: Partial<GitHubIssue> = {},
) {
const payload: LaunchPayload = { kind: "issue", issue: issue(issueOverrides), preset };
render(
<QuickTaskLauncher
workspaceId={WORKSPACE_ID}
workflows={[workflow]}
steps={[step]}
repositories={repositories}
payload={payload}
onClose={vi.fn()}
/>,
);
return mocks.dialogProps?.initialValues;
}
afterEach(() => {
mocks.dialogProps = undefined;
mocks.push.mockClear();
mocks.createTaskPR.mockClear();
mocks.linkTaskIssue.mockClear();
mocks.upsertTaskIssue.mockClear();
});
describe("QuickTaskLauncher repository defaults", () => {
it("falls back to Remote mode when the only matching repo is a task worktree path", () => {
const initialValues = renderLauncher([
repo({ id: "task-worktree", local_path: TASK_WORKTREE_PATH }),
]);
expect(initialValues).toMatchObject({
githubUrl: REPO_URL,
branch: PR_HEAD_BRANCH,
checkoutBranch: PR_HEAD_BRANCH,
prNumber: 1567,
prBaseBranch: "main",
});
expect(initialValues?.repositoryId).toBeUndefined();
});
it("opens PR launches in Remote mode even when a provider-backed repo exists", () => {
const initialValues = renderLauncher([
repo({ id: "task-worktree", local_path: TASK_WORKTREE_PATH }),
repo({
id: "provider-repo",
source_type: "provider",
local_path: "",
}),
]);
expect(initialValues).toMatchObject({
githubUrl: REPO_URL,
branch: PR_HEAD_BRANCH,
checkoutBranch: PR_HEAD_BRANCH,
prNumber: 1567,
prBaseBranch: "main",
});
expect(initialValues?.repositoryId).toBeUndefined();
});
it("still preselects an ordinary matching local GitHub repo for issues", () => {
const initialValues = renderIssueLauncher([
repo({ id: LOCAL_REPO_ID, local_path: "/work/kandev" }),
]);
expect(initialValues).toMatchObject({
repositoryId: LOCAL_REPO_ID,
});
expect(initialValues?.githubUrl).toBeUndefined();
});
it("rejects task worktree roots without a trailing slash", () => {
const initialValues = renderIssueLauncher([
repo({ id: "task-worktree-root", local_path: TASK_WORKTREE_ROOT }),
]);
expect(initialValues).toMatchObject({
githubUrl: "github.com/kdlbs/kandev",
});
expect(initialValues?.repositoryId).toBeUndefined();
});
it("derives a PR URL when the GitHub payload omits URL fields", () => {
const initialValues = renderLauncher(
[repo({ id: "task-worktree", local_path: TASK_WORKTREE_PATH })],
{ html_url: "", url: "" },
);
expect(initialValues).toMatchObject({
githubUrl: REPO_URL,
branch: PR_HEAD_BRANCH,
checkoutBranch: PR_HEAD_BRANCH,
prNumber: 1567,
prBaseBranch: "main",
});
expect(initialValues?.repositoryId).toBeUndefined();
});
});
describe("QuickTaskLauncher issue linking", () => {
it.each([true, false])(
"links and stores the launched issue with auto-focus %s",
async (autoFocus) => {
const link = {
task_id: "task-1",
task_title: "Review: add Download option",
owner: "kdlbs",
repo: "kandev",
issue_number: 1567,
issue_url: ISSUE_URL,
issue_title: "add Download option",
};
mocks.linkTaskIssue.mockResolvedValue(link);
renderIssueLauncher([repo({ id: LOCAL_REPO_ID })]);
mocks.dialogProps?.onSuccess?.({ id: "task-1" } as Task, "create", { autoFocus });
expect(mocks.linkTaskIssue).toHaveBeenCalledWith("task-1", { issue: ISSUE_URL });
await waitFor(() => {
expect(mocks.upsertTaskIssue).toHaveBeenCalledWith(WORKSPACE_ID, link);
});
expect(mocks.push).toHaveBeenCalledTimes(autoFocus ? 1 : 0);
if (autoFocus) expect(mocks.push).toHaveBeenCalledWith("/t/task-1");
},
);
it("navigates when issue linking fails", async () => {
mocks.linkTaskIssue.mockRejectedValueOnce(new Error("offline"));
renderIssueLauncher([repo({ id: LOCAL_REPO_ID })]);
mocks.dialogProps?.onSuccess?.({ id: "task-1" } as Task);
expect(mocks.push).toHaveBeenCalledWith("/t/task-1");
await waitFor(() => expect(mocks.linkTaskIssue).toHaveBeenCalledTimes(1));
expect(mocks.upsertTaskIssue).not.toHaveBeenCalled();
});
});