328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
|
import { TooltipProvider } from "@kandev/ui/tooltip";
|
|
import { StateProvider } from "@/components/state-provider";
|
|
import type { Repository } from "@/lib/types/http";
|
|
import type { DialogFormState, TaskRepoRow } from "./task-create-dialog-types";
|
|
import { WorkspaceRepoChips } from "./task-create-dialog-workspace-repo-chips";
|
|
|
|
vi.mock("@/hooks/domains/workspace/use-repository-branches", () => ({
|
|
useBranches: () => ({ branches: [], isLoading: false }),
|
|
}));
|
|
|
|
vi.mock("@/hooks/domains/workspace/use-repository-branch-policies", () => ({
|
|
useRepositoryBranchPolicies: () => ({
|
|
policies: [
|
|
{
|
|
id: "policy-1",
|
|
repository_id: "repo-front",
|
|
name: "Feature branches",
|
|
description: "Policy description",
|
|
base_branch: "main",
|
|
branch_template: "feature/{title}-{suffix}",
|
|
pull_request_target: "develop",
|
|
created_at: "2026-08-24T10:00:00Z",
|
|
updated_at: "2026-08-24T10:00:00Z",
|
|
},
|
|
],
|
|
}),
|
|
}));
|
|
|
|
const FRONTEND_ID = "repo-front";
|
|
const BACKEND_ID = "repo-back";
|
|
const CHIP_TRIGGER = "repo-chip-trigger";
|
|
const ADDED_MARKER = "already-added-repository-marker";
|
|
const DISCOVERED_PATH = "/home/me/projects/local-project";
|
|
const NOOP = (_key: string, _value: string) => undefined;
|
|
|
|
function repository(id: string, name: string): Repository {
|
|
return {
|
|
id,
|
|
workspace_id: "ws-1",
|
|
name,
|
|
source_type: "local",
|
|
local_path: `/repos/${name}`,
|
|
default_branch: "main",
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
} as Repository;
|
|
}
|
|
|
|
function row(overrides: Partial<TaskRepoRow> = {}): TaskRepoRow {
|
|
return { key: `row-${Math.random()}`, branch: "", ...overrides };
|
|
}
|
|
|
|
const repositories = [repository(FRONTEND_ID, "frontend"), repository(BACKEND_ID, "backend")];
|
|
const rows = [
|
|
row({ key: "r0", repositoryId: FRONTEND_ID, branch: "main" }),
|
|
row({ key: "r1", branch: "develop" }),
|
|
];
|
|
|
|
type ChipsProps = Parameters<typeof WorkspaceRepoChips>[0];
|
|
|
|
function chips(overrides: Partial<ChipsProps> = {}) {
|
|
return (
|
|
<WorkspaceRepoChips
|
|
rows={rows}
|
|
repositories={repositories}
|
|
workspaceId="ws-1"
|
|
canAddMore
|
|
onAdd={vi.fn()}
|
|
onRemove={vi.fn()}
|
|
onRowRepositoryChange={NOOP}
|
|
onRowBranchChange={NOOP}
|
|
{...overrides}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderChips(overrides: Partial<ChipsProps> = {}) {
|
|
return render(
|
|
<StateProvider>
|
|
<TooltipProvider>{chips(overrides)}</TooltipProvider>
|
|
</StateProvider>,
|
|
);
|
|
}
|
|
|
|
afterEach(cleanup);
|
|
|
|
describe("WorkspaceRepoChips duplicate policy", () => {
|
|
it("excludes repositories already selected by another quick-chat row", () => {
|
|
renderChips({ allowDuplicateRepositories: false });
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
|
|
expect(screen.queryByRole("option", { name: /^frontend/ })).toBeNull();
|
|
expect(screen.getByRole("option", { name: /^backend/ })).toBeTruthy();
|
|
});
|
|
|
|
it("keeps task creation's same-repository different-branch option", () => {
|
|
renderChips({ allowDuplicateRepositories: true });
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
|
|
expect(screen.getByRole("option", { name: /^frontend/ })).toBeTruthy();
|
|
expect(screen.getByRole("option", { name: /^backend/ })).toBeTruthy();
|
|
});
|
|
|
|
it("does not expose repository creation when the caller does not opt in", () => {
|
|
renderChips({ allowDuplicateRepositories: false });
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
|
|
expect(screen.queryByRole("button", { name: "Create new repository" })).toBeNull();
|
|
});
|
|
|
|
it("routes repository creation to the only row", () => {
|
|
const onCreateRepository = vi.fn();
|
|
renderChips({ rows: [rows[0]], onCreateRepository });
|
|
|
|
fireEvent.click(screen.getByTestId(CHIP_TRIGGER));
|
|
fireEvent.click(screen.getByTestId("create-local-repository-button"));
|
|
|
|
expect(onCreateRepository).toHaveBeenCalledWith("r0");
|
|
});
|
|
|
|
it("refreshes repositories from the selector toolbar", () => {
|
|
const onRefreshRepositories = vi.fn();
|
|
renderChips({ rows: [rows[0]], onRefreshRepositories });
|
|
|
|
fireEvent.click(screen.getByTestId(CHIP_TRIGGER));
|
|
fireEvent.click(screen.getByTestId("repo-refresh-button"));
|
|
|
|
expect(onRefreshRepositories).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("offers creation and refresh from a second repository row", () => {
|
|
const onCreateRepository = vi.fn();
|
|
const onRefreshRepositories = vi.fn();
|
|
renderChips({ onCreateRepository, onRefreshRepositories });
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
fireEvent.click(screen.getByTestId("repo-refresh-button"));
|
|
expect(onRefreshRepositories).toHaveBeenCalledOnce();
|
|
fireEvent.change(screen.getByPlaceholderText("Search repositories..."), {
|
|
target: { value: "no-matching-repository" },
|
|
});
|
|
fireEvent.click(screen.getByRole("button", { name: "Create new repository" }));
|
|
expect(onCreateRepository).toHaveBeenCalledWith("r1");
|
|
});
|
|
|
|
it.each([0, 1])("keeps both actions in empty lists on row %i while refreshing", (rowIndex) => {
|
|
renderChips({
|
|
repositories: [],
|
|
onCreateRepository: vi.fn(),
|
|
onRefreshRepositories: vi.fn(),
|
|
repositoriesRefreshing: true,
|
|
});
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[rowIndex]);
|
|
expect(screen.getByTestId("repo-refresh-button").hasAttribute("disabled")).toBe(true);
|
|
expect(screen.getByRole("button", { name: "Create new repository" })).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("WorkspaceRepoChips branch policy preview", () => {
|
|
it("keeps policy choices on one line and moves details behind an info control", () => {
|
|
renderChips({
|
|
rows: [row({ key: "r0", repositoryId: FRONTEND_ID, branch: "main" })],
|
|
showBranchPolicies: true,
|
|
});
|
|
|
|
fireEvent.click(screen.getByTestId("branch-chip-trigger"));
|
|
|
|
const option = screen.getByRole("option", { name: /Feature branches/ });
|
|
expect(option.textContent).not.toContain("feature/{title}-{suffix}");
|
|
expect(
|
|
screen.getByTestId("branch-policy-option-info-policy-1").getAttribute("aria-label"),
|
|
).toContain("feature/{title}-{suffix}");
|
|
});
|
|
});
|
|
|
|
describe("WorkspaceRepoChips saved base display", () => {
|
|
it("shows the saved base separately from a local executor checkout branch", () => {
|
|
renderChips({
|
|
rows: [
|
|
row({
|
|
key: "r0",
|
|
repositoryId: FRONTEND_ID,
|
|
branch: "feature/task",
|
|
baseBranch: "develop",
|
|
}),
|
|
],
|
|
isLocalExecutor: true,
|
|
});
|
|
|
|
expect(screen.getByTestId("repo-chip-base-branch").textContent).toContain("develop");
|
|
expect(screen.getByTestId("branch-chip-trigger").textContent).toContain("feature/task");
|
|
});
|
|
|
|
it("edits the local executor saved base without changing checkout state", () => {
|
|
const onRowBaseBranchChange = vi.fn();
|
|
const onRowBranchChange = vi.fn();
|
|
renderChips({
|
|
rows: [
|
|
row({
|
|
key: "r0",
|
|
repositoryId: FRONTEND_ID,
|
|
branch: "feature/task",
|
|
baseBranch: "develop",
|
|
}),
|
|
],
|
|
isLocalExecutor: true,
|
|
onRowBaseBranchChange,
|
|
onRowBranchChange,
|
|
});
|
|
|
|
fireEvent.click(screen.getByTestId("repo-chip-base-branch"));
|
|
fireEvent.click(screen.getByRole("option", { name: /Task default/ }));
|
|
|
|
expect(onRowBaseBranchChange).toHaveBeenCalledWith("r0", "");
|
|
expect(onRowBranchChange).not.toHaveBeenCalledWith("r0", "");
|
|
});
|
|
});
|
|
|
|
describe("WorkspaceRepoChips workspace markers", () => {
|
|
it("marks another task row's workspace repository while keeping it selectable", () => {
|
|
const onRowRepositoryChange = vi.fn();
|
|
renderChips({ allowDuplicateRepositories: true, onRowRepositoryChange });
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
|
|
const selectedElsewhere = screen.getByRole("option", { name: /^frontend/ });
|
|
const marker = within(selectedElsewhere).getByTestId(ADDED_MARKER);
|
|
expect(marker.getAttribute("aria-label")).toBe("Already added");
|
|
expect(marker.classList).toContain("text-primary");
|
|
fireEvent.click(selectedElsewhere);
|
|
expect(onRowRepositoryChange).toHaveBeenCalledWith("r1", FRONTEND_ID);
|
|
});
|
|
|
|
it("does not mark the only selected workspace repository when its row is reopened", () => {
|
|
renderChips({
|
|
allowDuplicateRepositories: true,
|
|
rows: [row({ key: "r0", repositoryId: FRONTEND_ID })],
|
|
});
|
|
fireEvent.click(screen.getByTestId(CHIP_TRIGGER));
|
|
fireEvent.keyDown(document, { key: "Escape" });
|
|
expect(screen.queryByRole("option", { name: /^frontend/ })).toBeNull();
|
|
fireEvent.click(screen.getByTestId(CHIP_TRIGGER));
|
|
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^frontend/ })).queryByTestId(ADDED_MARKER),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("clears a workspace marker when the selecting sibling changes or is removed", () => {
|
|
const { rerender } = renderChips({
|
|
allowDuplicateRepositories: true,
|
|
rows: [row({ key: "r0", repositoryId: FRONTEND_ID }), row({ key: "r1" })],
|
|
});
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^frontend/ })).getByTestId(ADDED_MARKER),
|
|
).toBeTruthy();
|
|
|
|
rerender(
|
|
<StateProvider>
|
|
<TooltipProvider>
|
|
{chips({ rows: [row({ key: "r0", repositoryId: BACKEND_ID }), row({ key: "r1" })] })}
|
|
</TooltipProvider>
|
|
</StateProvider>,
|
|
);
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^frontend/ })).queryByTestId(ADDED_MARKER),
|
|
).toBeNull();
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^backend/ })).getByTestId(ADDED_MARKER),
|
|
).toBeTruthy();
|
|
|
|
rerender(
|
|
<StateProvider>
|
|
<TooltipProvider>{chips({ rows: [row({ key: "r1" })] })}</TooltipProvider>
|
|
</StateProvider>,
|
|
);
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^backend/ })).queryByTestId(ADDED_MARKER),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("WorkspaceRepoChips discovered markers", () => {
|
|
it("marks normalized discovered paths selected by another task row and clears on rerender", () => {
|
|
const discoveredRepositories = [
|
|
{ path: DISCOVERED_PATH, name: "local-project" },
|
|
] as unknown as DialogFormState["discoveredRepositories"];
|
|
const { rerender } = renderChips({
|
|
allowDuplicateRepositories: true,
|
|
rows: [row({ key: "r0", localPath: `${DISCOVERED_PATH}/` }), row({ key: "r1" })],
|
|
discoveredRepositories,
|
|
});
|
|
fireEvent.click(screen.getAllByTestId(CHIP_TRIGGER)[1]);
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^local-project/ })).getByTestId(ADDED_MARKER),
|
|
).toBeTruthy();
|
|
|
|
rerender(
|
|
<StateProvider>
|
|
<TooltipProvider>
|
|
{chips({
|
|
rows: [
|
|
row({ key: "r0", localPath: "/home/me/projects/another-project" }),
|
|
row({ key: "r1" }),
|
|
],
|
|
discoveredRepositories,
|
|
})}
|
|
</TooltipProvider>
|
|
</StateProvider>,
|
|
);
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^local-project/ })).queryByTestId(ADDED_MARKER),
|
|
).toBeNull();
|
|
|
|
rerender(
|
|
<StateProvider>
|
|
<TooltipProvider>
|
|
{chips({ rows: [row({ key: "r1" })], discoveredRepositories })}
|
|
</TooltipProvider>
|
|
</StateProvider>,
|
|
);
|
|
expect(
|
|
within(screen.getByRole("option", { name: /^local-project/ })).queryByTestId(ADDED_MARKER),
|
|
).toBeNull();
|
|
});
|
|
});
|