// @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);
});
});