import { create } from "@bufbuild/protobuf"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import MemoMoveDialog from "@/components/MemoActionMenu/MemoMoveDialog"; import { MemoSchema, Visibility } from "@/types/proto/api/v1/memo_service_pb"; import { SpaceSchema } from "@/types/proto/api/v1/space_service_pb"; const state = vi.hoisted(() => ({ update: vi.fn(), refetch: vi.fn(), user: "users/alice" })); vi.mock("@/hooks/useCurrentUser", () => ({ default: () => ({ name: state.user }) })); vi.mock("@/hooks/useMemoQueries", () => ({ useUpdateMemo: () => ({ mutateAsync: state.update, isPending: false }) })); vi.mock("@/hooks/useSpaceQueries", () => ({ useSpaces: () => ({ data: [create(SpaceSchema, { name: "spaces/a", title: "Design" }), create(SpaceSchema, { name: "spaces/b", title: "Product" })], isPending: false, refetch: state.refetch, }), })); vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key })); vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn() } })); const memo = create(MemoSchema, { name: "memos/original", creator: "users/alice", space: "spaces/a", visibility: Visibility.SPACE }); const selectDestination = async (name: string) => { fireEvent.click(screen.getByRole("combobox", { name: "space.current" })); const option = await screen.findByRole("option", { name }); fireEvent.pointerDown(option, { pointerType: "mouse" }); fireEvent.click(option); }; describe("Move to Space", () => { beforeEach(() => { vi.clearAllMocks(); state.user = "users/alice"; state.update.mockResolvedValue(memo); }); it("requires a new destination and moves with explicit Space audience", async () => { const close = vi.fn(); render(); expect(screen.getByRole("button", { name: "memo.move.confirm" })).toBeDisabled(); await selectDestination("Product"); fireEvent.click(screen.getByRole("button", { name: "memo.move.confirm" })); await waitFor(() => expect(close).toHaveBeenCalledWith(false)); expect(state.update).toHaveBeenCalledWith({ update: { name: memo.name, space: "spaces/b", visibility: Visibility.SPACE }, updateMask: ["space", "visibility"], }); }); it("defaults to Private when unassigning a Space-visible memo", async () => { render(); await selectDestination("memo.move.unassigned"); fireEvent.click(screen.getByRole("button", { name: "memo.move.confirm" })); await waitFor(() => expect(state.update).toHaveBeenCalledWith({ update: { name: memo.name, space: "", visibility: Visibility.PRIVATE }, updateMask: ["space", "visibility"], }), ); }); it("preserves non-Space audience while assigning a memo", async () => { render(); await selectDestination("Product"); fireEvent.click(screen.getByRole("button", { name: "memo.move.confirm" })); await waitFor(() => expect(state.update).toHaveBeenCalledWith({ update: { name: memo.name, space: "spaces/b", visibility: Visibility.PUBLIC }, updateMask: ["space"], }), ); }); it("keeps the dialog open and refreshes destinations when a move is rejected", async () => { state.update.mockRejectedValue(new Error("Membership changed")); const close = vi.fn(); render(); await selectDestination("Product"); fireEvent.click(screen.getByRole("button", { name: "memo.move.confirm" })); expect(await screen.findByRole("alert")).toHaveTextContent("Membership changed"); expect(close).not.toHaveBeenCalled(); expect(state.refetch).toHaveBeenCalledOnce(); }); it("does not allow another user to submit a move", async () => { state.user = "users/bob"; render(); await selectDestination("Product"); expect(screen.getByRole("button", { name: "memo.move.confirm" })).toBeDisabled(); expect(state.update).not.toHaveBeenCalled(); }); });