/** * @file ProjectMemoryModal.test.tsx * @description Unit tests for `ProjectMemoryModal`: it must send the directory * the user typed (not a suggestion it picked for them), must refuse to submit * an empty one, and must render its labels from whichever namespace the * calling page passes — the Codex page shares this component with the Claude * page, and a key group that only exists in `ccConfig` would render raw. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ProjectMemoryModal } from "../ProjectMemoryModal"; import { api } from "../../../lib/api"; vi.mock("../../../lib/api", () => ({ api: { run: { cwds: vi.fn() } }, })); const cwds = vi.mocked(api.run.cwds); beforeEach(() => { cwds.mockResolvedValue({ items: [{ kind: "recent", path: "/home/me/known-project", label: "known-project" }], }); }); function renderModal(over: Partial[0]> = {}) { const onCreate = vi.fn().mockResolvedValue("/home/me/typed/CLAUDE.md"); render( {}} onCreate={onCreate} {...over} /> ); return { onCreate }; } describe("ProjectMemoryModal", () => { it("sends the typed directory and the edited content", async () => { const { onCreate } = renderModal(); await userEvent.type(screen.getByLabelText(/project directory/i), "/home/me/typed"); const body = screen.getByLabelText(/content/i); await userEvent.clear(body); await userEvent.type(body, "hello"); await userEvent.click(screen.getByRole("button", { name: /create/i })); expect(onCreate).toHaveBeenCalledWith({ dir: "/home/me/typed", content: "hello" }); }); it("refuses to submit without a directory", async () => { const { onCreate } = renderModal(); await userEvent.click(screen.getByRole("button", { name: /create/i })); expect(onCreate).not.toHaveBeenCalled(); expect(await screen.findByText(/directory is required/i)).toBeInTheDocument(); }); it("offers the dashboard's known projects as suggestions, not as the only choice", async () => { renderModal(); // A datalist suggests; it does not constrain. A project the dashboard has // never run in is precisely this modal's reason to exist, so the input // stays free text. const input = screen.getByLabelText(/project directory/i); expect(input.tagName).toBe("INPUT"); // jsdom exposes no accessible role for datalist options, so query the DOM. await vi.waitFor(() => { const list = document.getElementById(input.getAttribute("list")!); expect(list?.querySelector('option[value="/home/me/known-project"]')).not.toBeNull(); }); }); it("renders from the Codex namespace too", async () => { renderModal({ ns: "codexConfig", filename: "AGENTS.md" }); expect(await screen.findByText(/Create AGENTS\.md for another project/i)).toBeInTheDocument(); // A missing key renders as the bare key path — catch that directly. expect(screen.queryByText(/projectMemory\./)).not.toBeInTheDocument(); }); it("shows the server's rejection instead of closing", async () => { const onCreate = vi.fn().mockRejectedValue(new Error("CLAUDE.md already exists in /p")); render( {}} onCreate={onCreate} /> ); await userEvent.type(screen.getByLabelText(/project directory/i), "/p"); await userEvent.click(screen.getByRole("button", { name: /create/i })); expect(await screen.findByText(/already exists in \/p/i)).toBeInTheDocument(); }); });