// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { UserProfileResponse } from "@paperclipai/shared"; import { queryKeys } from "../lib/queryKeys"; import { UserProfile } from "./UserProfile"; const mockAuthApi = vi.hoisted(() => ({ getSession: vi.fn(), })); const mockUserProfilesApi = vi.hoisted(() => ({ get: vi.fn(), })); const mockSetBreadcrumbs = vi.hoisted(() => vi.fn()); const mockUseParams = vi.hoisted(() => vi.fn()); vi.mock("../api/auth", () => ({ authApi: mockAuthApi, })); vi.mock("../api/userProfiles", () => ({ userProfilesApi: mockUserProfilesApi, })); vi.mock("../context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs, }), })); vi.mock("../context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: "company-1", selectedCompany: { id: "company-1", name: "Paperclip", issuePrefix: "PAP" }, }), })); vi.mock("@/lib/router", () => ({ Link: ({ children, to, ...props }: { children: React.ReactNode; to: string }) => ( {children} ), useParams: () => mockUseParams(), })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; async function flushReact() { await act(async () => { await Promise.resolve(); await new Promise((resolve) => window.setTimeout(resolve, 0)); }); } function windowStats(key: "last7" | "last30" | "all", label: string): UserProfileResponse["stats"][number] { return { key, label, touchedIssues: 4, createdIssues: 2, completedIssues: 3, assignedOpenIssues: 1, commentCount: 5, activityCount: 9, costCents: 1200, inputTokens: 1000, cachedInputTokens: 200, outputTokens: 300, costEventCount: 2, }; } function profileFor(userId: string): UserProfileResponse { return { user: { id: userId, slug: "jane-example", name: "Jane Example", email: "jane@example.com", image: null, membershipRole: "owner", membershipStatus: "active", joinedAt: new Date("2026-01-05T00:00:00Z"), }, stats: [windowStats("last7", "Last 7 days"), windowStats("last30", "Last 30 days"), windowStats("all", "All time")], daily: [], recentIssues: [], recentActivity: [], topAgents: [], topProviders: [], }; } const EDIT_PROFILE_HREF = "/company/settings/instance/profile"; describe("UserProfile", () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); mockUseParams.mockReturnValue({ userSlug: "jane-example" }); mockAuthApi.getSession.mockResolvedValue({ session: { id: "session-1", userId: "user-1" }, user: { id: "user-1", name: "Jane Example", email: "jane@example.com", image: null, }, }); }); afterEach(() => { container.remove(); document.body.innerHTML = ""; vi.clearAllMocks(); }); async function render(profile: UserProfileResponse, hiddenSettings: string[] = []) { mockUserProfilesApi.get.mockResolvedValue(profile); const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); queryClient.setQueryData(queryKeys.health, { status: "ok", deploymentMode: "authenticated", hiddenSettings, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); return root; } it("offers an edit link on the viewer's own profile", async () => { const root = await render(profileFor("user-1")); expect(container.textContent).toContain("Jane Example"); const editLink = container.querySelector(`a[href="${EDIT_PROFILE_HREF}"]`); expect(editLink).not.toBeNull(); expect(editLink?.textContent).toContain("Edit profile"); expect(editLink?.querySelector("svg")?.classList).toContain("lucide-user-round-pen"); await act(async () => root.unmount()); }); it("keeps other users' profiles read-only", async () => { const root = await render(profileFor("user-2")); expect(container.textContent).toContain("Jane Example"); expect(container.querySelector(`a[href="${EDIT_PROFILE_HREF}"]`)).toBeNull(); expect(container.textContent).not.toContain("Edit profile"); await act(async () => root.unmount()); }); it("drops the edit link when the operator hides profile settings", async () => { const root = await render(profileFor("user-1"), ["instance.profile"]); expect(container.textContent).toContain("Jane Example"); expect(container.querySelector(`a[href="${EDIT_PROFILE_HREF}"]`)).toBeNull(); await act(async () => root.unmount()); }); });