import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import SpaceSwitcher from "@/components/AppSidebar/SpaceSwitcher"; const spaceState = vi.hoisted(() => ({ spaces: [ { name: "spaces/product", title: "Product", description: "" }, { name: "spaces/research", title: "Research", description: "" }, ], selectedSpace: undefined as { name: string; title: string; description: string } | undefined, selectedSpaceName: undefined as string | undefined, selectMemos: vi.fn(), selectSpace: vi.fn(), })); vi.mock("@/components/MemosLogo", () => ({ default: () => Memos, })); vi.mock("@/components/CreateSpaceDialog", () => ({ default: ({ open, onCreated }: { open: boolean; onCreated?: (space: (typeof spaceState.spaces)[number]) => void }) => open ? (
Create Space dialog
) : null, })); vi.mock("@/contexts/SpaceContext", () => ({ useSpaceContext: () => { const duplicateSpaceTitles = new Set( spaceState.spaces .filter((space, index) => spaceState.spaces.findIndex((candidate) => candidate.title === space.title) !== index) .map((space) => space.title), ); return { ...spaceState, duplicateSpaceTitles, isLoadingSpaces: false, isSpacesError: false, }; }, })); vi.mock("@/utils/i18n", () => ({ useTranslate: () => (key: string) => key, })); describe("SpaceSwitcher", () => { beforeEach(() => { spaceState.spaces = [ { name: "spaces/product", title: "Product", description: "" }, { name: "spaces/research", title: "Research", description: "" }, ]; spaceState.selectedSpace = undefined; spaceState.selectedSpaceName = undefined; spaceState.selectMemos.mockClear(); spaceState.selectSpace.mockClear(); }); it("lists Memos, every available Space, and the create entry", async () => { render(); fireEvent.click(screen.getByRole("button", { name: "space.switch: common.memos" })); expect(await screen.findByRole("menuitemradio", { name: "Memos" })).toHaveAttribute("aria-checked", "true"); const productRow = screen.getByRole("menuitemradio", { name: "Product" }); expect(productRow).toHaveAttribute("aria-checked", "false"); expect(productRow.querySelector(".lucide-astroid")).not.toBeNull(); expect(screen.getByRole("menuitemradio", { name: "Research" })).toBeInTheDocument(); expect(screen.getByRole("menuitem", { name: "space.create" })).toBeInTheDocument(); }); it("marks exactly one context as active", async () => { spaceState.selectedSpaceName = "spaces/product"; spaceState.selectedSpace = spaceState.spaces[0]; render(); fireEvent.click(screen.getByRole("button", { name: "space.switch: Product" })); const rows = await screen.findAllByRole("menuitemradio"); expect(rows.map((row) => row.getAttribute("aria-checked"))).toEqual(["false", "true", "false"]); // The active row carries the fill; the check is its only indicator. const active = screen.getByRole("menuitemradio", { name: "Product" }); expect(active.className).toContain("bg-accent/60"); expect(active.querySelector(".lucide-check")).not.toBeNull(); expect(screen.getByRole("menuitemradio", { name: "Research" }).querySelector(".lucide-check")).toBeNull(); }); it("shows UIDs only for Spaces whose titles match", async () => { const uuid = "123e4567-e89b-12d3-a456-426614174000"; spaceState.spaces = [ { name: "spaces/product-notes", title: "Product", description: "" }, { name: `spaces/${uuid}`, title: "Product", description: "" }, { name: "spaces/research-space", title: "Research", description: "" }, ]; spaceState.selectedSpaceName = spaceState.spaces[0].name; spaceState.selectedSpace = spaceState.spaces[0]; render(); const trigger = screen.getByRole("button", { name: "space.switch: Product (product-notes)" }); expect(within(trigger).getByTitle("product-notes")).toHaveTextContent("product-notes"); fireEvent.click(trigger); const customIdRow = await screen.findByRole("menuitemradio", { name: "Product (product-notes)" }); const uuidRow = screen.getByRole("menuitemradio", { name: `Product (${uuid})` }); expect(within(customIdRow).getByTitle("product-notes")).toHaveTextContent("product-notes"); expect(within(uuidRow).getByTitle(uuid)).toHaveTextContent("123e4567…"); const researchRow = screen.getByRole("menuitemradio", { name: "Research" }); expect(within(researchRow).queryByTitle("research-space")).not.toBeInTheDocument(); expect(researchRow).not.toHaveTextContent("research-space"); }); it("switches context without navigation and opens Space creation", async () => { render(); fireEvent.click(screen.getByRole("button", { name: "space.switch: common.memos" })); fireEvent.click(await screen.findByRole("menuitemradio", { name: "Product" })); expect(spaceState.selectSpace).toHaveBeenCalledWith(spaceState.spaces[0]); await waitFor(() => expect(screen.queryByRole("menu")).not.toBeInTheDocument()); // Selecting Memos is how a signed-in user gets back to the home feed. fireEvent.click(screen.getByRole("button", { name: "space.switch: common.memos" })); fireEvent.click(await screen.findByRole("menuitemradio", { name: "Memos" })); expect(spaceState.selectMemos).toHaveBeenCalledOnce(); await waitFor(() => expect(screen.queryByRole("menu")).not.toBeInTheDocument()); fireEvent.click(screen.getByRole("button", { name: "space.switch: common.memos" })); fireEvent.click(await screen.findByRole("menuitem", { name: "space.create" })); expect(screen.getByRole("dialog", { name: "" })).toHaveTextContent("Create Space dialog"); fireEvent.click(screen.getByRole("button", { name: "Complete create" })); expect(spaceState.selectSpace).toHaveBeenCalledWith(spaceState.spaces[0]); }); });