Files
kandev/apps/web/components/gitlab/my-gitlab/quick-task-launcher.test.tsx
T
2026-09-14 13:01:29 +00:00

256 lines
8.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { act, render, screen } from "@testing-library/react";
import { StateProvider } from "@/components/state-provider";
import { ToastProvider } from "@/components/toast-provider";
import type { Task } from "@/lib/types/http";
import type { GitLabLaunchPayload } from "./quick-task-launcher";
const createTaskMRMock = vi.fn();
const pushMock = vi.fn();
let dialogProps: Record<string, unknown> | null = null;
const WORKSPACE_ID = "ws-1";
const WORKFLOW_ID = "workflow-1";
const FIXED_DATE = "2026-01-01T00:00:00Z";
const PROJECT_NAMESPACE = "group/subgroup";
const PROJECT_PATH = `${PROJECT_NAMESPACE}/project`;
vi.mock("@/lib/api/domains/gitlab-api", () => ({
createTaskMR: (...args: unknown[]) => createTaskMRMock(...args),
}));
vi.mock("@/lib/routing/client-router", () => ({ useRouter: () => ({ push: pushMock }) }));
vi.mock("@/components/task-create-dialog", () => ({
TaskCreateDialog: (props: Record<string, unknown>) => {
dialogProps = props;
return null;
},
}));
import { QuickTaskLauncher } from "./quick-task-launcher";
const preset = {
id: "review",
label: "Review",
hint: "Review changes",
icon: () => null,
prompt: ({ url }: { url: string; title: string }) => `Review ${url}`,
};
const payload: GitLabLaunchPayload = {
kind: "mr",
preset,
mr: {
id: 1,
iid: 7,
project_id: 10,
title: "Review this",
url: "https://gitlab.acme/group/subgroup/project/-/merge_requests/7",
web_url: "https://gitlab.acme/group/subgroup/project/-/merge_requests/7",
state: "opened",
head_branch: "feature",
head_sha: "abc",
base_branch: "main",
author_username: "alice",
project_namespace: PROJECT_NAMESPACE,
project_path: PROJECT_PATH,
body: "",
draft: false,
merge_status: "can_be_merged",
has_conflicts: false,
additions: 1,
deletions: 0,
reviewers: [],
assignees: [],
created_at: FIXED_DATE,
updated_at: FIXED_DATE,
},
};
describe("GitLab QuickTaskLauncher", () => {
beforeEach(() => {
dialogProps = null;
createTaskMRMock.mockReset().mockResolvedValue({ id: "association-1" });
pushMock.mockReset();
});
it.each([true, false])("links the created task with auto-focus %s", async (autoFocus) => {
render(
<StateProvider>
<ToastProvider>
<QuickTaskLauncher
workspaceId={WORKSPACE_ID}
configuredHost="https://gitlab.acme"
workflows={[{ id: WORKFLOW_ID, workspace_id: WORKSPACE_ID, name: "Default" } as never]}
steps={[{ id: "step-1", workflow_id: WORKFLOW_ID, name: "Todo", position: 0 } as never]}
repositories={[
{
id: "repo-host-collision",
workspace_id: WORKSPACE_ID,
provider: "gitlab",
provider_host: "https://gitlab.example.net",
provider_owner: PROJECT_NAMESPACE,
provider_name: "project",
default_branch: "main",
} as never,
{
id: "repo-project-collision",
workspace_id: WORKSPACE_ID,
provider: "gitlab",
provider_host: "https://gitlab.acme",
provider_owner: "other/group",
provider_name: "project",
default_branch: "main",
} as never,
{
id: "repo-1",
workspace_id: WORKSPACE_ID,
provider: "gitlab",
provider_host: "https://gitlab.acme/",
provider_owner: PROJECT_NAMESPACE,
provider_name: "project",
default_branch: "main",
} as never,
]}
payload={payload}
onClose={vi.fn()}
/>
</ToastProvider>
</StateProvider>,
);
expect(dialogProps?.initialValues).toMatchObject({
title: "Review: Review this",
repositoryId: "repo-1",
branch: "feature",
checkoutBranch: "feature",
});
await act(async () => {
await (
dialogProps?.onSuccess as (
task: Task,
mode: "create",
meta: { autoFocus: boolean },
) => Promise<void>
)(
{
id: "task-1",
repositories: [{ repository_id: "repo-1" }],
} as Task,
"create",
{ autoFocus },
);
});
expect(createTaskMRMock).toHaveBeenCalledWith(
{
task_id: "task-1",
repository_id: "repo-1",
mr_url: payload.mr.web_url,
},
WORKSPACE_ID,
);
expect(pushMock).toHaveBeenCalledTimes(autoFocus ? 1 : 0);
if (autoFocus) expect(pushMock).toHaveBeenCalledWith("/t/task-1");
});
});
describe("GitLab QuickTaskLauncher recovery", () => {
beforeEach(() => {
dialogProps = null;
createTaskMRMock.mockReset().mockResolvedValue({ id: "association-1" });
pushMock.mockReset();
});
it("does not persist a durable association for issue launches", async () => {
const issuePayload: GitLabLaunchPayload = {
kind: "issue",
preset,
issue: {
id: 2,
iid: 3,
project_id: 10,
title: "Fix bug",
body: "",
url: "https://gitlab.acme/group/project/-/issues/3",
web_url: "https://gitlab.acme/group/project/-/issues/3",
state: "opened",
author_username: "alice",
project_namespace: "group",
project_path: "group/project",
labels: [],
assignees: [],
milestone: "",
created_at: FIXED_DATE,
updated_at: FIXED_DATE,
},
};
render(
<StateProvider>
<ToastProvider>
<QuickTaskLauncher
workspaceId={WORKSPACE_ID}
configuredHost="https://gitlab.acme"
workflows={[{ id: WORKFLOW_ID, workspace_id: WORKSPACE_ID, name: "Default" } as never]}
steps={[{ id: "step-1", workflow_id: WORKFLOW_ID, name: "Todo", position: 0 } as never]}
repositories={[]}
payload={issuePayload}
onClose={vi.fn()}
/>
</ToastProvider>
</StateProvider>,
);
await act(async () => {
await (dialogProps?.onSuccess as (task: Task) => Promise<void>)({
id: "task-issue",
repositories: [],
} as unknown as Task);
});
expect(createTaskMRMock).not.toHaveBeenCalled();
expect(dialogProps?.initialValues).toMatchObject({
description: expect.stringContaining(issuePayload.issue.web_url),
});
});
it("keeps task recovery coherent and reports a partial failure when MR linking fails", async () => {
createTaskMRMock.mockRejectedValueOnce(new Error("repository origin does not match"));
render(
<StateProvider>
<ToastProvider>
<QuickTaskLauncher
workspaceId={WORKSPACE_ID}
configuredHost="https://gitlab.acme"
workflows={[{ id: WORKFLOW_ID, workspace_id: WORKSPACE_ID, name: "Default" } as never]}
steps={[{ id: "step-1", workflow_id: WORKFLOW_ID, name: "Todo", position: 0 } as never]}
repositories={[
{
id: "repo-1",
workspace_id: WORKSPACE_ID,
provider: "gitlab",
provider_host: "https://gitlab.acme",
provider_owner: PROJECT_NAMESPACE,
provider_name: "project",
default_branch: "main",
} as never,
]}
payload={payload}
onClose={vi.fn()}
/>
</ToastProvider>
</StateProvider>,
);
await act(async () => {
await (dialogProps?.onSuccess as (task: Task) => Promise<void>)({
id: "task-partial",
repositories: [{ repository_id: "repo-1" }],
} as Task);
});
expect(pushMock).toHaveBeenCalledWith("/t/task-partial");
expect(screen.getByText("Task created, but merge request was not linked")).toBeTruthy();
expect(
screen.getByText(/Open the task and use Link GitLab merge request to retry/),
).toBeTruthy();
});
});