// @vitest-environment jsdom import React from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("next-intl", () => ({ useTranslations: () => (key: string, values?: Record) => { if (values) return `${key}:${JSON.stringify(values)}`; return key; }, })); // Mock shared components vi.mock("@/shared/components", () => ({ Card: ({ children }: { children: React.ReactNode }) => React.createElement("div", { className: "card" }, children), Badge: ({ children, variant, title }: { children: React.ReactNode; variant?: string; title?: string }) => React.createElement("span", { "data-variant": variant, title }, children), Button: ({ children, onClick, disabled, loading, variant, size, "data-testid": testId, }: { children?: React.ReactNode; onClick?: () => void; disabled?: boolean; loading?: boolean; variant?: string; size?: string; "data-testid"?: string; }) => React.createElement( "button", { onClick, disabled: disabled || loading, "data-variant": variant, "data-testid": testId, }, children, ), Input: ({ value, onChange, placeholder, "data-testid": testId, className, onKeyDown, }: { value?: string; onChange?: (e: React.ChangeEvent) => void; placeholder?: string; "data-testid"?: string; className?: string; onKeyDown?: (e: React.KeyboardEvent) => void; }) => React.createElement("input", { value, onChange, placeholder, "data-testid": testId, className, onKeyDown, }), Select: ({ children, value, onChange, }: { children: React.ReactNode; value?: string; onChange?: (e: React.ChangeEvent) => void; }) => React.createElement("select", { value, onChange }, children), Modal: ({ isOpen, title, children, footer, onClose, }: { isOpen?: boolean; title?: string; children?: React.ReactNode; footer?: React.ReactNode; onClose?: () => void; }) => isOpen ? React.createElement( "div", { "data-testid": "modal", "data-title": title }, React.createElement("button", { onClick: onClose, "data-testid": "modal-close" }, "X"), children, footer, ) : null, })); vi.mock( "../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal", () => ({ default: ({ isOpen, onClose, }: { isOpen: boolean; memory: unknown; onClose: () => void; onSaved: () => void; }) => isOpen ? React.createElement("div", { "data-testid": "edit-memory-modal" }, [ React.createElement("button", { key: "close", onClick: onClose }, "close"), ]) : null, }), ); const MOCK_MEMORIES = [ { id: "mem-1", apiKeyId: "key-1", sessionId: null, type: "factual", key: "user.name", content: "Alice", metadata: {}, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", expiresAt: null, }, { id: "mem-2", apiKeyId: "key-1", sessionId: null, type: "episodic", key: "event.meeting", content: "Had a meeting", metadata: {}, createdAt: "2026-01-02T00:00:00Z", updatedAt: "2026-01-02T00:00:00Z", expiresAt: null, }, ]; const cleanupCallbacks: Array<() => void> = []; function makeContainer(): HTMLElement { const container = document.createElement("div"); document.body.appendChild(container); cleanupCallbacks.push(() => container.remove()); return container; } describe("MemoriesTab", () => { beforeEach(() => { (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: MOCK_MEMORIES, total: 2, totalPages: 1, stats: { total: 2, tokensUsed: 150, hitRate: 0.75, cacheStats: { hits: 3, misses: 1 }, }, }), }); }); afterEach(() => { while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.(); document.body.innerHTML = ""; vi.clearAllMocks(); }); it("renders memories after fetch", async () => { const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); // Wait for the 300ms debounce await act(async () => { await new Promise((r) => setTimeout(r, 350)); }); expect(container.textContent).toContain("user.name"); expect(container.textContent).toContain("Alice"); }); it("shows hit rate card when cacheStats.hits + misses > 0", async () => { const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); await act(async () => { await new Promise((r) => setTimeout(r, 350)); }); // hitRate is shown since cacheStats.hits=3, misses=1 expect(container.textContent).toContain("hitRate"); }); it("does not show hit rate when cacheStats is 0/0", async () => { vi.resetModules(); globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: MOCK_MEMORIES, total: 2, totalPages: 1, stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 }, }, }), }); const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); await act(async () => { await new Promise((r) => setTimeout(r, 350)); }); // hitRate card should NOT appear expect(container.querySelector("[data-testid='hit-rate-card']")).toBeNull(); }); it("opens edit modal when pencil button is clicked", async () => { const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); await act(async () => { await new Promise((r) => setTimeout(r, 350)); }); const editBtn = container.querySelector("[data-testid='edit-memory-mem-1']") as HTMLButtonElement | null; expect(editBtn).toBeTruthy(); await act(async () => { editBtn?.click(); }); expect(container.querySelector("[data-testid='edit-memory-modal']")).toBeTruthy(); }); it("shows empty state when no memories returned", async () => { vi.resetModules(); globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [], total: 0, totalPages: 1, stats: { total: 0, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } }, }), }); const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); await act(async () => { await new Promise((r) => setTimeout(r, 350)); }); expect(container.querySelector("[data-testid='memories-empty-state']")).toBeTruthy(); }); it("calls DELETE when delete confirmed", async () => { const mockFetch = vi.fn(); mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ data: MOCK_MEMORIES, total: 2, totalPages: 1, stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } }, }), }); mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) }); globalThis.fetch = mockFetch; const { default: MemoriesTab } = await import( "../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); await act(async () => { await new Promise((r) => setTimeout(r, 350)); }); const deleteBtn = container.querySelector("[data-testid='delete-memory-mem-1']") as HTMLButtonElement | null; expect(deleteBtn).toBeTruthy(); await act(async () => { deleteBtn?.click(); }); // Confirm modal shown const modal = container.querySelector("[data-testid='modal']"); expect(modal).toBeTruthy(); // Find danger button inside modal const dangerBtns = Array.from(container.querySelectorAll("button")).filter( (b) => b.getAttribute("data-variant") === "danger", ); expect(dangerBtns.length).toBeGreaterThan(0); await act(async () => { dangerBtns[0].click(); }); // DELETE should have been called const deleteCalls = (mockFetch as ReturnType).mock.calls.filter( (c: [string, ...unknown[]]) => typeof c[0] === "string" && c[0].includes("mem-1") && c[1] && (c[1] as { method: string }).method === "DELETE", ); expect(deleteCalls.length).toBeGreaterThan(0); }); });