Files
kandev/apps/web/components/task/chat/entity-reference-menu.test.tsx
T

229 lines
6.7 KiB
TypeScript

import type { ComponentType } from "react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { EntityReference, EntityReferenceSearchGroup } from "@/lib/types/entity-reference";
import * as entityReferenceMenus from "./entity-reference-menu";
afterEach(cleanup);
const unavailableSearchGroups: EntityReferenceSearchGroup[] = [
{
source: "gitlab_issues",
provider: "gitlab",
kind: "issue",
display_name: "GitLab",
kind_label: "Issue",
status: "unsupported_scope",
results: [],
},
{
source: "linear_issues",
provider: "linear",
kind: "issue",
display_name: "Linear",
kind_label: "Issue",
status: "not_configured",
results: [],
},
{
source: "github_issues",
provider: "github",
kind: "issue",
display_name: "GitHub",
kind_label: "Issue",
status: "timeout",
results: [],
},
];
const kandevTaskGroup: EntityReferenceSearchGroup = {
source: "kandev_tasks",
provider: "kandev",
kind: "task",
display_name: "Kandev tasks",
kind_label: "Task",
status: "ok",
results: [
{
version: 1,
ref: "mention:v1:kandev:task:workspace-1:task-1",
provider: "kandev",
kind: "task",
id: "task-1",
key: "",
title: "Existing task",
url: "/t/task-1",
scope: "workspace-1",
},
],
};
const githubIssueGroup: EntityReferenceSearchGroup = {
source: "github_issues",
provider: "github",
kind: "issue",
display_name: "GitHub",
kind_label: "Issue",
status: "ok",
results: [
{
version: 1,
ref: "mention:v1:github:issue:octo%2Frepo:7",
provider: "github",
kind: "issue",
id: "7",
key: "#7",
title: "External issue",
url: "https://github.com/octo/repo/issues/7",
scope: "octo/repo",
},
],
};
type EntityReferenceMenuProps = {
isOpen: boolean;
clientRect: () => DOMRect;
groups: EntityReferenceSearchGroup[];
query: string;
selectedIndex: number;
isSearching: boolean;
error: null;
onRetry: () => void;
onSelect: (reference: EntityReference) => void;
onClose: () => void;
setSelectedIndex: (index: number) => void;
};
const EntityReferenceMenu =
entityReferenceMenus.EntityReferenceMenu as unknown as ComponentType<EntityReferenceMenuProps>;
function renderEntityReferenceMenu(
groups: EntityReferenceSearchGroup[],
onSelect: (reference: EntityReference) => void,
) {
render(
<EntityReferenceMenu
isOpen
clientRect={() => new DOMRect(16, 240, 1, 20)}
groups={groups}
query="auth"
selectedIndex={0}
isSearching={false}
error={null}
onRetry={vi.fn()}
onSelect={onSelect}
onClose={vi.fn()}
setSelectedIndex={vi.fn()}
/>,
);
}
describe("EntityReferenceMenu", () => {
it("provides a menu distinct from @ context mentions", () => {
expect(typeof (entityReferenceMenus as Record<string, unknown>).EntityReferenceMenu).toBe(
"function",
);
});
it("renders descriptor-driven groups with a generic fallback and 44px touch rows", async () => {
const reference: EntityReference = {
version: 1,
ref: "mention:v1:plugin:acme:incident:scope:incident-9",
provider: "plugin:acme:tracker",
kind: "incident",
id: "incident-9",
key: "INC-9",
title: "Authentication outage",
url: "https://tracker.example.test/incidents/9",
scope: "tracker.example.test/team-a",
};
const groups: EntityReferenceSearchGroup[] = [
{
source: "plugin:acme:incidents",
provider: reference.provider,
kind: reference.kind,
display_name: "Acme tracker",
kind_label: "Incident",
status: "ok",
results: [reference],
},
];
const onSelect = vi.fn();
renderEntityReferenceMenu(groups, onSelect);
expect(screen.getByText("Acme tracker")).toBeTruthy();
expect(screen.getByTestId("entity-reference-menu")).toBeTruthy();
expect(screen.getByText("Incident")).toBeTruthy();
expect(screen.getByTestId("entity-reference-generic-icon")).toBeTruthy();
const row = await screen.findByRole("option", { name: /#INC-9.*Authentication outage/ });
expect(row.className).toContain("min-h-11");
fireEvent.click(row);
expect(onSelect).toHaveBeenCalledWith(reference);
});
it("renders a plugin pull request without a provider-specific composer branch", async () => {
const reference: EntityReference = {
version: 1,
ref: "mention:v1:plugin%3Aexample%3Areviews:pull_request:workspace-1:pull-42",
provider: "plugin:example:reviews",
kind: "pull_request",
id: "pull-42",
key: "PR-42",
title: "Fix authentication",
url: "https://reviews.example.test/pull-requests/42",
scope: "workspace-1",
};
const onSelect = vi.fn();
renderEntityReferenceMenu(
[
{
source: "plugin:example:pull-requests",
provider: reference.provider,
kind: reference.kind,
display_name: "Example reviews",
kind_label: "Pull request",
status: "ok",
results: [reference],
},
],
onSelect,
);
const row = await screen.findByRole("option", { name: /#PR-42.*Fix authentication/ });
expect(screen.getByText("Example reviews")).toBeTruthy();
expect(screen.getByText("Pull request")).toBeTruthy();
expect(row.querySelector("svg")?.getAttribute("class")).toContain("h-4");
expect(row.querySelector("svg")?.getAttribute("class")).toContain("w-4");
fireEvent.click(row);
expect(onSelect).toHaveBeenCalledWith(reference);
});
it("omits sources that are not searchable in the active workspace", () => {
const visibleEntityReferenceGroups = (entityReferenceMenus as Record<string, unknown>)
.visibleEntityReferenceGroups;
expect(visibleEntityReferenceGroups).toBeTypeOf("function");
if (typeof visibleEntityReferenceGroups !== "function") return;
expect(
(visibleEntityReferenceGroups as (value: EntityReferenceSearchGroup[]) => unknown)(
unavailableSearchGroups,
),
).toEqual([unavailableSearchGroups[2]]);
});
it("keeps Kandev tasks out of # work item search", () => {
expect(entityReferenceMenus.visibleEntityReferenceGroups([kandevTaskGroup])).toEqual([]);
const selectableEntityReferences = (entityReferenceMenus as Record<string, unknown>)
.selectableEntityReferences;
expect(selectableEntityReferences).toBeTypeOf("function");
if (typeof selectableEntityReferences !== "function") return;
expect(
(selectableEntityReferences as (value: EntityReferenceSearchGroup[]) => unknown)([
kandevTaskGroup,
githubIssueGroup,
]),
).toEqual(githubIssueGroup.results);
});
});