// @vitest-environment jsdom import { act, cleanup, fireEvent, render, waitFor } from "@testing-library/react"; import * as React from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { Button } from "../primitives/button"; import { Command, CommandCollection, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command"; afterEach(cleanup); const commands = ["Calendar", "Calculator", "Settings"]; function ActionList({ onSelect = () => {} }: { onSelect?: (value: string) => void }) { return ( No commands found. {(item: string) => ( {item} )} ); } describe("Base UI Command", () => { it("blocks read-only command actions and restores activation when readOnly is cleared", async () => { const onSelect = vi.fn(); const onClick = vi.fn(); const content = (readOnly: boolean) => ( {(item: string) => ( {item} )} ); const view = render(content(true)); const input = view.getByRole("combobox"); act(() => input.focus()); await waitFor(() => expect(view.getByRole("option").hasAttribute("data-highlighted")).toBe(true)); fireEvent.keyDown(input, { key: "Enter" }); expect(onSelect).not.toHaveBeenCalled(); expect(onClick).not.toHaveBeenCalled(); fireEvent.click(view.getByRole("option")); expect(onSelect).not.toHaveBeenCalled(); expect(onClick).not.toHaveBeenCalled(); view.rerender(content(false)); fireEvent.click(view.getByRole("option")); expect(onSelect.mock.calls).toEqual([["Settings"]]); expect(onClick).toHaveBeenCalledTimes(1); }); it("filters commands, announces an empty result and restores the collection", async () => { const view = render(); const input = view.getByRole("combobox", { name: "Search commands" }); expect(view.getAllByRole("option")).toHaveLength(3); const status = view.getByRole("status"); expect(status.textContent).toBe(""); fireEvent.change(input, { target: { value: "SET" } }); await waitFor(() => expect(view.getAllByRole("option")).toHaveLength(1)); expect(view.getByRole("option").textContent).toBe("Settings"); fireEvent.change(input, { target: { value: "missing" } }); await waitFor(() => expect(view.queryAllByRole("option")).toHaveLength(0)); expect(status.textContent).toBe("No commands found."); expect(status.getAttribute("aria-live")).toBe("polite"); fireEvent.change(input, { target: { value: "" } }); await waitFor(() => expect(view.getAllByRole("option")).toHaveLength(3)); expect(view.getByRole("status")).toBe(status); expect(status.textContent).toBe(""); }); it("keeps input focus, blocks a highlighted disabled command and activates an enabled one once", async () => { const onSelect = vi.fn(); const view = render(); const input = view.getByRole("combobox"); act(() => input.focus()); await waitFor(() => expect(view.getByRole("option", { name: "Calendar" }).hasAttribute("data-highlighted")).toBe(true)); fireEvent.keyDown(input, { key: "ArrowDown" }); await waitFor(() => expect(view.getByRole("option", { name: "Calculator" }).hasAttribute("data-highlighted")).toBe(true)); fireEvent.keyDown(input, { key: "Enter" }); expect(onSelect).not.toHaveBeenCalled(); fireEvent.keyDown(input, { key: "ArrowDown" }); await waitFor(() => expect(view.getByRole("option", { name: "Settings" }).hasAttribute("data-highlighted")).toBe(true)); fireEvent.keyDown(input, { key: "Enter" }); expect(onSelect.mock.calls).toEqual([["Settings"]]); expect(document.activeElement).toBe(input); expect((input as HTMLInputElement).value).toBe(""); }); it("blocks disabled pointer activation and preserves the typed query after a command", async () => { const onSelect = vi.fn(); const view = render(); fireEvent.click(view.getByRole("option", { name: "Calculator" })); expect(onSelect).not.toHaveBeenCalled(); const input = view.getByRole("combobox"); fireEvent.change(input, { target: { value: "cal" } }); fireEvent.click(await view.findByRole("option", { name: "Calendar" })); expect(onSelect.mock.calls).toEqual([["Calendar"]]); expect((input as HTMLInputElement).value).toBe("cal"); }); it("filters grouped object items and removes empty group headings", async () => { const groups = [ { label: "Places", items: [{ label: "Café" }] }, { label: "Workspace", items: [{ label: "Settings" }] }, ]; const view = render( items={groups} itemToStringValue={(item) => item.label}> No commands found. {(group: typeof groups[number]) => ( {(item: { label: string }) => {item.label}} )} , ); fireEvent.change(view.getByRole("combobox"), { target: { value: "cafe" } }); await waitFor(() => expect(view.getAllByRole("option")).toHaveLength(1)); expect(view.getByRole("group", { name: "Places" })).toBeTruthy(); expect(view.queryByRole("group", { name: "Workspace" })).toBeNull(); }); it("uses controlled search state and updates filtering after external changes", async () => { const onValueChange = vi.fn(); function Controlled({ query }: { query: string }) { return ( {(item: string) => {item}} ); } const view = render(); expect(view.getAllByRole("option")).toHaveLength(2); fireEvent.change(view.getByRole("combobox"), { target: { value: "settings" } }); expect(onValueChange.mock.calls[0]?.[0]).toBe("settings"); view.rerender(); await waitFor(() => expect(view.getAllByRole("option")).toHaveLength(1)); expect((view.getByRole("combobox") as HTMLInputElement).value).toBe("settings"); }); it("focuses a dialog search and restores trigger focus after Escape and an action", async () => { function Example() { const [open, setOpen] = React.useState(false); return ( Open commands}> setOpen(false)} /> ); } const view = render(); const trigger = view.getByRole("button", { name: "Open commands" }); act(() => trigger.focus()); fireEvent.click(trigger); const input = await view.findByRole("combobox"); await waitFor(() => expect(document.activeElement).toBe(input)); fireEvent.keyDown(input, { key: "Escape" }); await waitFor(() => expect(view.queryByRole("dialog")).toBeNull()); await waitFor(() => expect(document.activeElement).toBe(trigger)); fireEvent.click(trigger); const reopenedInput = await view.findByRole("combobox"); await waitFor(() => expect(document.activeElement).toBe(reopenedInput)); fireEvent.keyDown(reopenedInput, { key: "Enter" }); await waitFor(() => expect(view.queryByRole("dialog")).toBeNull()); await waitFor(() => expect(document.activeElement).toBe(trigger)); }); it("forwards imperative refs to the root, input, list and item", () => { const rootRef = React.createRef(); const inputRef = React.createRef(); const listRef = React.createRef(); const itemRef = React.createRef(); const view = render( {(item: string) => {item}} , ); expect(rootRef.current?.contains(inputRef.current)).toBe(true); expect(inputRef.current).toBe(view.getByRole("combobox")); expect(listRef.current).toBe(view.getByRole("listbox")); expect(itemRef.current).toBe(view.getByRole("option")); act(() => inputRef.current?.focus()); expect(document.activeElement).toBe(inputRef.current); }); });