import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook } from "@testing-library/react"; import type { PropsWithChildren } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useMemoSave } from "@/components/MemoEditor/hooks/useMemoSave"; const mocks = vi.hoisted(() => ({ dispatch: vi.fn(), markNewMemo: vi.fn(), memoSave: vi.fn(), })); vi.mock("@/components/MemoEditor/services", () => ({ errorService: { getErrorMessage: () => "save failed" }, memoService: { save: mocks.memoSave }, validationService: { canSave: () => ({ valid: true }) }, })); vi.mock("@/components/MemoEditor/state", () => ({ useEditorContext: () => ({ actions: { reset: () => ({ type: "reset" }), setLoading: (key: string, value: boolean) => ({ type: "set-loading", key, value }), setJustSaved: (value: boolean) => ({ type: "set-just-saved", value }), setMetadata: () => ({ type: "set-metadata" }), setTimestamps: () => ({ type: "set-timestamps" }), }, dispatch: mocks.dispatch, getState: () => ({ ui: { justSaved: false } }), }), })); vi.mock("@/contexts/NewMemoContext", () => ({ useNewMemo: () => ({ markNewMemo: mocks.markNewMemo }), })); vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key, })); describe("useMemoSave", () => { beforeEach(() => { mocks.dispatch.mockReset(); mocks.markNewMemo.mockReset(); mocks.memoSave.mockReset(); }); it("invalidates scoped attachment libraries after a memo save", async () => { mocks.memoSave.mockResolvedValue({ hasChanges: true, memoName: "memos/new" }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries"); const wrapper = ({ children }: PropsWithChildren) => {children}; const discardDraft = vi.fn(); const { result } = renderHook(() => useMemoSave({ discardDraft }), { wrapper }); await act(async () => result.current()); expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["attachments", "list"] }); expect(discardDraft).toHaveBeenCalledOnce(); expect(mocks.markNewMemo).toHaveBeenCalledWith("memos/new"); }); it("refreshes the parent memo total after creating a comment", async () => { mocks.memoSave.mockResolvedValue({ hasChanges: true, memoName: "memos/comment" }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries"); const wrapper = ({ children }: PropsWithChildren) => {children}; const { result } = renderHook(() => useMemoSave({ parentMemoName: "memos/parent", discardDraft: vi.fn() }), { wrapper }); await act(async () => result.current()); expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["memos", "comments", "memos/parent"] }); expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["memos", "detail", "memos/parent"] }); }); it("holds a saved confirmation before a closing host resets", async () => { mocks.memoSave.mockResolvedValue({ hasChanges: true, memoName: "memos/existing" }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const wrapper = ({ children }: PropsWithChildren) => {children}; const onConfirm = vi.fn(); const { result } = renderHook(() => useMemoSave({ memoName: "memos/existing", discardDraft: vi.fn(), onConfirm }), { wrapper }); await act(async () => result.current()); const types = mocks.dispatch.mock.calls.map(([action]) => action); const savedOn = types.findIndex((a) => a.type === "set-just-saved" && a.value === true); const reset = types.findIndex((a) => a.type === "reset"); expect(savedOn).toBeGreaterThan(-1); expect(reset).toBeGreaterThan(savedOn); expect(onConfirm).toHaveBeenCalledWith("memos/existing"); }); it("does not hold the in-place composer after saving a new memo", async () => { mocks.memoSave.mockResolvedValue({ hasChanges: true, memoName: "memos/new" }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const wrapper = ({ children }: PropsWithChildren) => {children}; const { result } = renderHook(() => useMemoSave({ discardDraft: vi.fn() }), { wrapper }); await act(async () => result.current()); const savedOn = mocks.dispatch.mock.calls.some(([action]) => action.type === "set-just-saved" && action.value === true); expect(savedOn).toBe(false); }); });