388 lines
13 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { Dropdown } from "#/ui/dropdown/dropdown";
const mockOptions = [
{ value: "1", label: "Option 1" },
{ value: "2", label: "Option 2" },
{ value: "3", label: "Option 3" },
];
describe("Dropdown", () => {
describe("Trigger", () => {
it("should render a custom trigger button", () => {
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
expect(trigger).toBeInTheDocument();
});
it("should open dropdown on trigger click", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
const listbox = screen.getByRole("listbox");
expect(listbox).toBeInTheDocument();
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
});
describe("Type-ahead / Search", () => {
it("should filter options based on input text", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
const input = screen.getByRole("combobox");
await user.type(input, "Option 1");
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
expect(screen.queryByText("Option 3")).not.toBeInTheDocument();
});
it("should be case-insensitive by default", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
const input = screen.getByRole("combobox");
await user.type(input, "option 1");
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
});
it("should show all options when search is cleared", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
const input = screen.getByRole("combobox");
await user.type(input, "Option 1");
await user.clear(input);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
});
describe("Empty state", () => {
it("should display empty state when no options provided", async () => {
const user = userEvent.setup();
render(<Dropdown options={[]} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
expect(screen.getByText("No options")).toBeInTheDocument();
});
it("should render custom empty state message", async () => {
const user = userEvent.setup();
render(<Dropdown options={[]} emptyMessage="Nothing found" />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
expect(screen.getByText("Nothing found")).toBeInTheDocument();
});
});
describe("Single selection", () => {
it("should select an option on click", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
const option = screen.getByText("Option 1");
await user.click(option);
expect(screen.getByRole("combobox")).toHaveValue("Option 1");
});
it("should close dropdown after selection", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByText("Option 1"));
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
});
it("should display selected option in input", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByText("Option 1"));
expect(screen.getByRole("combobox")).toHaveValue("Option 1");
});
it("should highlight currently selected option in list", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByRole("option", { name: "Option 1" }));
await user.click(trigger);
const selectedOption = screen.getByRole("option", { name: "Option 1" });
expect(selectedOption).toHaveAttribute("aria-selected", "true");
});
it("should preserve selected value in input and show all options when reopening dropdown", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByRole("option", { name: "Option 1" }));
// Reopen the dropdown
await user.click(trigger);
const input = screen.getByRole("combobox");
expect(input).toHaveValue("Option 1");
expect(
screen.getByRole("option", { name: "Option 1" }),
).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Option 2" }),
).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Option 3" }),
).toBeInTheDocument();
});
});
describe("Clear button", () => {
it("should not render clear button by default", () => {
render(<Dropdown options={mockOptions} />);
expect(screen.queryByTestId("dropdown-clear")).not.toBeInTheDocument();
});
it("should render clear button when clearable prop is true and has value", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} clearable />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByRole("option", { name: "Option 1" }));
expect(screen.getByTestId("dropdown-clear")).toBeInTheDocument();
});
it("should clear selection and search input when clear button is clicked", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} clearable />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByRole("option", { name: "Option 1" }));
const clearButton = screen.getByTestId("dropdown-clear");
await user.click(clearButton);
expect(screen.getByRole("combobox")).toHaveValue("");
});
it("should not render clear button when there is no selection", () => {
render(<Dropdown options={mockOptions} clearable />);
expect(screen.queryByTestId("dropdown-clear")).not.toBeInTheDocument();
});
it("should show placeholder after clearing selection", async () => {
const user = userEvent.setup();
render(
<Dropdown
options={mockOptions}
clearable
placeholder="Select an option"
/>,
);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByRole("option", { name: "Option 1" }));
const clearButton = screen.getByTestId("dropdown-clear");
await user.click(clearButton);
const input = screen.getByRole("combobox");
expect(input).toHaveValue("");
});
});
describe("Loading state", () => {
it("should not display loading indicator by default", () => {
render(<Dropdown options={mockOptions} />);
expect(screen.queryByTestId("dropdown-loading")).not.toBeInTheDocument();
});
it("should display loading indicator when loading prop is true", () => {
render(<Dropdown options={mockOptions} loading />);
expect(screen.getByTestId("dropdown-loading")).toBeInTheDocument();
});
it("should disable interaction while loading", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} loading />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "false");
});
});
describe("Disabled state", () => {
it("should not open dropdown when disabled", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} disabled />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "false");
});
it("should have disabled attribute on trigger", () => {
render(<Dropdown options={mockOptions} disabled />);
const trigger = screen.getByTestId("dropdown-trigger");
expect(trigger).toBeDisabled();
});
});
describe("Placeholder", () => {
it("should display placeholder text when no value selected", () => {
render(<Dropdown options={mockOptions} placeholder="Select an option" />);
const input = screen.getByRole("combobox");
expect(input).toHaveAttribute("placeholder", "Select an option");
});
});
describe("Default value", () => {
it("should display defaultValue in input on mount", () => {
render(<Dropdown options={mockOptions} defaultValue={mockOptions[0]} />);
const input = screen.getByRole("combobox");
expect(input).toHaveValue("Option 1");
});
it("should show all options when opened with defaultValue", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} defaultValue={mockOptions[0]} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
expect(
screen.getByRole("option", { name: "Option 1" }),
).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Option 2" }),
).toBeInTheDocument();
expect(
screen.getByRole("option", { name: "Option 3" }),
).toBeInTheDocument();
});
it("should restore input value when closed with Escape", async () => {
const user = userEvent.setup();
render(<Dropdown options={mockOptions} defaultValue={mockOptions[0]} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
const input = screen.getByRole("combobox");
await user.type(input, "test");
await user.keyboard("{Escape}");
expect(input).toHaveValue("Option 1");
});
});
describe("onChange", () => {
it("should call onChange with selected item when option is clicked", async () => {
const user = userEvent.setup();
const onChangeMock = vi.fn();
render(<Dropdown options={mockOptions} onChange={onChangeMock} />);
const trigger = screen.getByTestId("dropdown-trigger");
await user.click(trigger);
await user.click(screen.getByRole("option", { name: "Option 1" }));
expect(onChangeMock).toHaveBeenCalledWith(mockOptions[0]);
});
it("should call onChange with null when selection is cleared", async () => {
const user = userEvent.setup();
const onChangeMock = vi.fn();
render(
<Dropdown
options={mockOptions}
clearable
defaultValue={mockOptions[0]}
onChange={onChangeMock}
/>,
);
const clearButton = screen.getByTestId("dropdown-clear");
await user.click(clearButton);
expect(onChangeMock).toHaveBeenCalledWith(null);
});
});
describe("Controlled mode", () => {
it.todo("should reflect external value changes");
it.todo("should call onChange when selection changes");
it.todo("should not update internal state when controlled");
});
describe("Uncontrolled mode", () => {
it.todo("should manage selection state internally");
it.todo("should call onChange when selection changes");
it.todo("should support defaultValue prop");
});
describe("testId prop", () => {
it("should apply custom testId to the root container", () => {
render(<Dropdown options={mockOptions} testId="org-dropdown" />);
expect(screen.getByTestId("org-dropdown")).toBeInTheDocument();
});
});
});