import { forwardRef, useRef, useState, useImperativeHandle } from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { Issue, TimelineEntry } from "@multica/core/types"; // useWorkspaceId() derives from useCurrentWorkspace (relative import inside // @multica/core/hooks.tsx). vi.mock("@multica/core/paths") only intercepts // the bare-specifier, not the internal relative import. Mock the hooks module // directly so the bridge hook returns the test UUID. vi.mock("@multica/core/hooks", () => ({ useWorkspaceId: () => "ws-1", })); // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- // Mock @multica/core/auth const mockAuthUser = { id: "user-1", email: "test@test.com", name: "Test User" }; vi.mock("@multica/core/auth", () => ({ useAuthStore: Object.assign( (selector?: any) => { const state = { user: mockAuthUser, isAuthenticated: true }; return selector ? selector(state) : state; }, { getState: () => ({ user: mockAuthUser, isAuthenticated: true }) }, ), registerAuthStore: vi.fn(), createAuthStore: vi.fn(), })); // Mock @multica/core/workspace/hooks vi.mock("@multica/core/workspace/hooks", () => ({ useActorName: () => ({ getMemberName: (id: string) => (id === "user-1" ? "Test User" : "Unknown"), getAgentName: (id: string) => (id === "agent-1" ? "Claude Agent" : "Unknown Agent"), getActorName: (type: string, id: string) => { if (type === "member" && id === "user-1") return "Test User"; if (type === "agent" && id === "agent-1") return "Claude Agent"; return "Unknown"; }, getActorInitials: (type: string) => (type === "member" ? "TU" : "CA"), getActorAvatarUrl: () => null, }), })); // Mock workspace queries vi.mock("@multica/core/workspace/queries", () => ({ memberListOptions: () => ({ queryKey: ["workspaces", "ws-1", "members"], queryFn: () => Promise.resolve([{ user_id: "user-1", name: "Test User", email: "test@test.com", role: "admin" }]), }), agentListOptions: () => ({ queryKey: ["workspaces", "ws-1", "agents"], queryFn: () => Promise.resolve([]), }), assigneeFrequencyOptions: () => ({ queryKey: ["workspaces", "ws-1", "assignee-frequency"], queryFn: () => Promise.resolve([]), }), workspaceListOptions: () => ({ queryKey: ["workspaces"], queryFn: () => Promise.resolve([{ id: "ws-1", name: "Test WS", slug: "test" }]), }), })); // Mock @multica/core/paths — after the URL-driven workspace refactor, // useCurrentWorkspace / useWorkspacePaths derive from the workspace slug in // URL Context. Tests don't mount a real route, so we short-circuit to fixtures. vi.mock("@multica/core/paths", async () => { const actual = await vi.importActual( "@multica/core/paths", ); return { ...actual, useCurrentWorkspace: () => ({ id: "ws-1", name: "Test WS", slug: "test" }), useWorkspacePaths: () => actual.paths.workspace("test"), }; }); // Mock navigation vi.mock("../../navigation", () => ({ AppLink: ({ children, href, ...props }: any) => ( {children} ), useNavigation: () => ({ push: vi.fn(), pathname: "/issues/issue-1", getShareableUrl: undefined }), NavigationProvider: ({ children }: { children: React.ReactNode }) => children, })); // Mock editor components (Tiptap requires real DOM) vi.mock("../../editor", () => ({ useFileDropZone: () => ({ isDragOver: false, dropZoneProps: {} }), FileDropOverlay: () => null, ReadonlyContent: ({ content }: { content: string }) => (
{content}
), ContentEditor: forwardRef(function MockContentEditor( { defaultValue, onUpdate, placeholder }: any, ref: any, ) { const valueRef = useRef(defaultValue || ""); const [value, setValue] = useState(defaultValue || ""); useImperativeHandle(ref, () => ({ getMarkdown: () => valueRef.current, clearContent: () => { valueRef.current = ""; setValue(""); }, focus: () => {}, uploadFile: () => {}, })); return (