mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-31 00:40:46 +02:00
152 lines
7.7 KiB
TypeScript
152 lines
7.7 KiB
TypeScript
import { render, screen, waitFor } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { ApiError } from "@multica/core/api";
|
|
import { AppSidebar } from "./app-sidebar";
|
|
|
|
const { detail, deletePin, pins } = vi.hoisted(() => ({
|
|
detail: { current: { isPending: false, isError: false, data: null as unknown, error: null as unknown } },
|
|
deletePin: vi.fn(),
|
|
pins: {
|
|
current: [
|
|
{
|
|
id: "pin-1",
|
|
workspace_id: "ws-1",
|
|
user_id: "user-1",
|
|
item_type: "issue" as const,
|
|
item_id: "issue-1",
|
|
position: 0,
|
|
created_at: "2026-05-06T00:00:00Z",
|
|
},
|
|
],
|
|
},
|
|
}));
|
|
|
|
vi.mock("@dnd-kit/core", () => ({
|
|
DndContext: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
PointerSensor: vi.fn(),
|
|
closestCenter: vi.fn(),
|
|
useSensor: vi.fn(),
|
|
useSensors: vi.fn(),
|
|
}));
|
|
vi.mock("@dnd-kit/sortable", () => ({
|
|
SortableContext: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
useSortable: () => ({ attributes: {}, listeners: {}, setNodeRef: vi.fn() }),
|
|
verticalListSortingStrategy: vi.fn(),
|
|
}));
|
|
vi.mock("@dnd-kit/utilities", () => ({ CSS: { Transform: { toString: () => undefined } } }));
|
|
vi.mock("@multica/ui/components/ui/sidebar", () => ({
|
|
Sidebar: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarFooter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarGroup: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarGroupContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarGroupLabel: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarHeader: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarMenu: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarMenuButton: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
|
SidebarMenuItem: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
SidebarRail: () => null,
|
|
}));
|
|
vi.mock("@multica/ui/components/ui/dropdown-menu", () => ({
|
|
DropdownMenu: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
DropdownMenuContent: () => null,
|
|
DropdownMenuGroup: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
DropdownMenuItem: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
DropdownMenuLabel: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
DropdownMenuSeparator: () => null,
|
|
DropdownMenuTrigger: ({ render }: { render: React.ReactNode }) => <>{render}</>,
|
|
}));
|
|
vi.mock("@multica/ui/components/ui/collapsible", () => ({
|
|
Collapsible: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
CollapsibleContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
CollapsibleTrigger: () => <button type="button" />,
|
|
}));
|
|
vi.mock("@multica/ui/components/ui/tooltip", () => ({
|
|
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <button type="button">{children}</button>,
|
|
}));
|
|
vi.mock("./help-launcher", () => ({ HelpLauncher: () => null }));
|
|
vi.mock("../auth", () => ({ useLogout: () => vi.fn() }));
|
|
vi.mock("../issues/components/status-icon", () => ({ StatusIcon: () => <span /> }));
|
|
vi.mock("../navigation", () => ({
|
|
AppLink: ({ children, href }: { children: React.ReactNode; href: string }) => <a href={href}>{children}</a>,
|
|
useNavigation: () => ({ pathname: "/acme/issues", push: vi.fn() }),
|
|
}));
|
|
vi.mock("../projects/components/project-icon", () => ({ ProjectIcon: () => <span /> }));
|
|
vi.mock("../workspace/workspace-avatar", () => ({ WorkspaceAvatar: () => <span /> }));
|
|
vi.mock("@multica/ui/components/common/actor-avatar", () => ({ ActorAvatar: () => <span /> }));
|
|
|
|
vi.mock("@multica/core/auth", () => ({
|
|
useAuthStore: (selector: (state: { user: { id: string } }) => unknown) => selector({ user: { id: "user-1" } }),
|
|
}));
|
|
vi.mock("@multica/core/paths", () => ({
|
|
paths: { workspace: (slug: string) => ({ issues: () => `/${slug}/issues` }) },
|
|
useCurrentWorkspace: () => ({ id: "ws-1", name: "Acme", slug: "acme" }),
|
|
useWorkspacePaths: () => ({
|
|
inbox: () => "/acme/inbox",
|
|
myIssues: () => "/acme/my-issues",
|
|
issues: () => "/acme/issues",
|
|
projects: () => "/acme/projects",
|
|
autopilots: () => "/acme/autopilots",
|
|
agents: () => "/acme/agents",
|
|
squads: () => "/acme/squads",
|
|
usage: () => "/acme/usage",
|
|
runtimes: () => "/acme/runtimes",
|
|
skills: () => "/acme/skills",
|
|
settings: () => "/acme/settings",
|
|
issueDetail: (id: string) => `/acme/issues/${id}`,
|
|
projectDetail: (id: string) => `/acme/projects/${id}`,
|
|
}),
|
|
}));
|
|
vi.mock("@multica/core/api", async (importOriginal) => ({ ...(await importOriginal<typeof import("@multica/core/api")>()), api: {} }));
|
|
vi.mock("@multica/core/inbox/queries", () => ({ deduplicateInboxItems: (items: unknown[]) => items, inboxKeys: { list: () => ["inbox"] } }));
|
|
vi.mock("@multica/core/issues/queries", () => ({ issueDetailOptions: () => ({ queryKey: ["issue"] }) }));
|
|
vi.mock("@multica/core/issues/stores/create-mode-store", () => ({ useCreateModeStore: { getState: () => ({ lastMode: "agent" }) } }));
|
|
vi.mock("@multica/core/issues/stores/draft-store", () => ({ useIssueDraftStore: () => false }));
|
|
vi.mock("@multica/core/modals", () => ({ useModalStore: { getState: () => ({ modal: null, open: vi.fn() }) } }));
|
|
vi.mock("@multica/core/pins/mutations", () => ({ useDeletePin: () => ({ mutate: deletePin }), useReorderPins: () => ({ mutate: vi.fn() }) }));
|
|
vi.mock("@multica/core/pins/queries", () => ({ pinListOptions: () => ({ queryKey: ["pins"] }) }));
|
|
vi.mock("@multica/core/projects/queries", () => ({ projectDetailOptions: () => ({ queryKey: ["project"] }) }));
|
|
vi.mock("@multica/core/runtimes/hooks", () => ({ useMyRuntimesNeedUpdate: () => false }));
|
|
vi.mock("@multica/core/workspace/queries", () => ({
|
|
myInvitationListOptions: () => ({ queryKey: ["invitations"] }),
|
|
workspaceKeys: { myInvitations: () => ["invitations"] },
|
|
workspaceListOptions: () => ({ queryKey: ["workspaces"] }),
|
|
}));
|
|
vi.mock("@tanstack/react-query", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("@tanstack/react-query")>()),
|
|
useMutation: () => ({ isPending: false, mutate: vi.fn() }),
|
|
useQuery: ({ queryKey }: { queryKey: readonly unknown[] }) => {
|
|
if (queryKey[0] === "pins") return { data: pins.current };
|
|
if (queryKey[0] === "issue") return detail.current;
|
|
return { data: [] };
|
|
},
|
|
useQueryClient: () => ({ fetchQuery: vi.fn(), invalidateQueries: vi.fn() }),
|
|
}));
|
|
|
|
describe("PinRow", () => {
|
|
beforeEach(() => {
|
|
deletePin.mockReset();
|
|
detail.current = { isPending: false, isError: false, data: null, error: null };
|
|
});
|
|
|
|
it("unpins missing details", async () => {
|
|
detail.current = { isPending: false, isError: true, data: null, error: new ApiError("missing", 404, "Not Found") };
|
|
render(<AppSidebar />);
|
|
await waitFor(() => expect(deletePin).toHaveBeenCalledTimes(1));
|
|
});
|
|
|
|
it("ignores non-404 errors", async () => {
|
|
detail.current = { isPending: false, isError: true, data: null, error: new ApiError("error", 500, "Server Error") };
|
|
render(<AppSidebar />);
|
|
await waitFor(() => expect(deletePin).not.toHaveBeenCalled());
|
|
});
|
|
|
|
it("renders loaded details", async () => {
|
|
detail.current = { isPending: false, isError: false, data: { identifier: "MUL-123", title: "Keep this pin", status: "todo" }, error: null };
|
|
render(<AppSidebar />);
|
|
expect(await screen.findByText("MUL-123 Keep this pin")).toBeInTheDocument();
|
|
});
|
|
});
|