import { renderHook } from "@testing-library/react"; import type { ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Mock dependencies BEFORE importing the hook under test. vi.mock("@/hooks/useUserQueries", () => ({ useAllUserStats: vi.fn(), useUserStats: vi.fn(), })); vi.mock("@/hooks/useMemoQueries", () => ({ useMemos: () => ({ data: undefined, isLoading: false }), })); vi.mock("@/hooks/useCurrentUser", () => ({ default: () => ({ name: "users/test", id: 1 }), })); const mockUseView = vi.fn(); vi.mock("@/contexts/ViewContext", async () => { const actual = await vi.importActual("@/contexts/ViewContext"); return { ...actual, useView: () => mockUseView(), }; }); import { useFilteredMemoStats } from "@/hooks/useFilteredMemoStats"; import { useAllUserStats, useUserStats } from "@/hooks/useUserQueries"; import { State } from "@/types/proto/api/v1/common_pb"; const wrapper = ({ children }: { children: ReactNode }) => children as never; const ts = (year: number, month: number, day: number) => ({ seconds: BigInt(Math.floor(Date.UTC(year, month - 1, day) / 1000)), nanos: 0, }); describe("useFilteredMemoStats", () => { beforeEach(() => { vi.mocked(useAllUserStats).mockReturnValue({ data: [], isLoading: false, } as unknown as ReturnType); vi.mocked(useUserStats).mockReturnValue({ data: { memoCreatedTimestamps: [ts(2026, 5, 1), ts(2026, 5, 1), ts(2026, 5, 2)], memoUpdatedTimestamps: [ts(2026, 5, 3), ts(2026, 5, 3), ts(2026, 5, 3)], tagCount: {}, }, isLoading: false, } as unknown as ReturnType); }); afterEach(() => { vi.clearAllMocks(); }); it("aggregates by created timestamps when timeBasis is create_time", () => { mockUseView.mockReturnValue({ timeBasis: "create_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); const { result } = renderHook(() => useFilteredMemoStats({ userName: "users/test" }), { wrapper }); expect(result.current.statistics.activityStats).toEqual({ "2026-05-01": 2, "2026-05-02": 1 }); expect(result.current.statistics.timeBasis).toBe("create_time"); }); it("aggregates by updated timestamps when timeBasis is update_time", () => { mockUseView.mockReturnValue({ timeBasis: "update_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); const { result } = renderHook(() => useFilteredMemoStats({ userName: "users/test" }), { wrapper }); expect(result.current.statistics.activityStats).toEqual({ "2026-05-03": 3 }); expect(result.current.statistics.timeBasis).toBe("update_time"); }); it("falls back to created timestamps when updated array is empty (old server)", () => { // Old servers that don't know about the new field deserialize it as []. // Length divergence (created non-empty, updated empty) is the signal. vi.mocked(useUserStats).mockReturnValue({ data: { memoCreatedTimestamps: [ts(2026, 5, 1)], memoUpdatedTimestamps: [], tagCount: {}, }, isLoading: false, } as unknown as ReturnType); mockUseView.mockReturnValue({ timeBasis: "update_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); const { result } = renderHook(() => useFilteredMemoStats({ userName: "users/test" }), { wrapper }); expect(result.current.statistics.activityStats).toEqual({ "2026-05-01": 1 }); expect(warn).toHaveBeenCalled(); warn.mockRestore(); }); it("disables both statistics queries when deferred", () => { mockUseView.mockReturnValue({ timeBasis: "create_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); renderHook(() => useFilteredMemoStats({ userName: "users/test", context: "explore", enabled: false }), { wrapper }); expect(useUserStats).toHaveBeenCalledWith("users/test", { enabled: false, filter: undefined }); expect(useAllUserStats).toHaveBeenCalledWith(expect.anything(), { enabled: false }); }); it("passes the selected Space filter to Home statistics", () => { mockUseView.mockReturnValue({ timeBasis: "create_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); const filter = 'space == "spaces/product"'; renderHook(() => useFilteredMemoStats({ userName: "users/test", context: "home", filter }), { wrapper }); expect(useUserStats).toHaveBeenCalledWith("users/test", { enabled: true, filter }); }); it("scopes Explore statistics and includes the Space audience", () => { mockUseView.mockReturnValue({ timeBasis: "create_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); const filter = 'space == "spaces/product"'; renderHook(() => useFilteredMemoStats({ context: "explore", filter, includeSpaceVisibility: true }), { wrapper }); expect(useAllUserStats).toHaveBeenCalledWith( { state: State.NORMAL, filter: '(space == "spaces/product") && (visibility in ["PUBLIC", "PROTECTED", "SPACE"])', }, { enabled: true }, ); }); it("uses the no-Space filter for Memos statistics", () => { mockUseView.mockReturnValue({ timeBasis: "create_time", orderByTimeAsc: false, setOrderByTimeAsc: vi.fn(), setTimeBasis: vi.fn(), }); const filter = "space == null"; renderHook(() => useFilteredMemoStats({ context: "archived", filter }), { wrapper }); expect(useAllUserStats).toHaveBeenCalledWith({ state: State.ARCHIVED, filter }, { enabled: true }); }); });