import { render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { MultiSelect, MultiSelectContent, MultiSelectItem, MultiSelectSelectAll, MultiSelectTrigger, MultiSelectValue, } from "./multiselect"; const scrollIntoViewMock = vi.fn(); class ResizeObserverMock { observe() {} unobserve() {} disconnect() {} } Object.defineProperty(globalThis, "ResizeObserver", { writable: true, configurable: true, value: ResizeObserverMock, }); Object.defineProperty(HTMLElement.prototype, "scrollIntoView", { writable: true, configurable: true, value: scrollIntoViewMock, }); describe("MultiSelect", () => { beforeEach(() => { scrollIntoViewMock.mockClear(); }); it("shows preselected labels before the popover opens", () => { // Given render( {}}> Production AWS Development Azure , ); // Then expect( within(screen.getByRole("combobox")).getByText("Production AWS"), ).toBeInTheDocument(); expect( within(screen.getByRole("combobox")).queryByText("Select accounts"), ).not.toBeInTheDocument(); }); it("keeps the filter label context when a value is selected", () => { render( {}}> FAIL PASS , ); expect( within(screen.getByRole("combobox")).getByText("Status"), ).toBeInTheDocument(); expect( within(screen.getByRole("combobox")).getByText("FAIL"), ).toBeInTheDocument(); expect( within(screen.getByRole("combobox")).queryByText("All Status"), ).not.toBeInTheDocument(); }); it("filters items without crashing when search is enabled", async () => { const user = userEvent.setup(); render( {}}> Production AWS Development Azure , ); await user.click(screen.getByRole("combobox")); await user.type(screen.getByPlaceholderText("Search accounts..."), "aws"); expect( within(screen.getByRole("dialog")).getByText("Production AWS"), ).toBeInTheDocument(); expect( within(screen.getByRole("dialog")).queryByText("Development Azure"), ).not.toBeInTheDocument(); }); it("scrolls the first visible match into view when filtering", async () => { const user = userEvent.setup(); render( {}}> Development Azure Production AWS , ); await user.click(screen.getByRole("combobox")); await user.type(screen.getByPlaceholderText("Search accounts..."), "aws"); await waitFor(() => { expect(scrollIntoViewMock).toHaveBeenCalled(); }); }); it("clears the search input when reopening the popover", async () => { const user = userEvent.setup(); render( {}}> Production AWS Development Azure , ); await user.click(screen.getByRole("combobox")); const searchInput = screen.getByPlaceholderText( "Search accounts...", ) as HTMLInputElement; await user.type(searchInput, "aws"); expect(searchInput).toHaveValue("aws"); await user.keyboard("{Escape}"); expect( screen.queryByPlaceholderText("Search accounts..."), ).not.toBeInTheDocument(); await user.click(screen.getByRole("combobox")); expect(screen.getByPlaceholderText("Search accounts...")).toHaveValue(""); }); it("closes the dropdown when clicking outside", async () => { // Given const user = userEvent.setup(); render(
{}}> Production AWS
, ); // When await user.click(screen.getByRole("combobox")); expect(screen.getByPlaceholderText("Search accounts...")).toBeVisible(); await user.click(screen.getByRole("button", { name: /outside target/i })); // Then expect( screen.queryByPlaceholderText("Search accounts..."), ).not.toBeInTheDocument(); }); it("uses a normalized dropdown width instead of growing with the longest item", async () => { const user = userEvent.setup(); render( {}}> This is a very long option label that should not expand the dropdown indefinitely , ); await user.click(screen.getByRole("combobox")); expect(screen.getByRole("dialog")).toHaveClass( "w-[min(var(--radix-popover-trigger-width),calc(100vw-2rem))]", ); expect(screen.getByRole("dialog")).toHaveClass("max-w-[24rem]"); }); it("keeps long option lists scrollable inside the dropdown", async () => { // Given const user = userEvent.setup(); render( {}}> {Array.from({ length: 20 }, (_, index) => ( Account {index} ))} , ); // When await user.click(screen.getByRole("combobox")); // Then const list = screen .getByRole("dialog") .querySelector('[data-slot="command-list"]'); expect(screen.getByRole("dialog")).toHaveStyle({ maxHeight: "min(360px, var(--radix-popover-content-available-height, 360px))", }); expect(list).toHaveClass("minimal-scrollbar"); expect(list).toHaveStyle({ maxHeight: "min(300px, var(--radix-popover-content-available-height, 300px))", }); expect(list).toHaveClass("overflow-y-auto"); expect(list).toHaveClass("overscroll-contain"); }); it("keeps the legacy clear-all behavior by default", async () => { const user = userEvent.setup(); const onValuesChange = vi.fn(); render( Select All Production AWS Development Azure , ); await user.click(screen.getByRole("combobox")); await user.click(screen.getByRole("button", { name: /select all/i })); expect(onValuesChange).toHaveBeenCalledWith([]); }); it("disables the legacy select all action when no filter is selected", async () => { const user = userEvent.setup(); const onValuesChange = vi.fn(); render( Select All Production AWS Development Azure , ); await user.click(screen.getByRole("combobox")); expect( screen.getByRole("button", { name: /all selected/i }), ).toBeDisabled(); }); it("selects every provided option when select mode is enabled", async () => { const user = userEvent.setup(); const onValuesChange = vi.fn(); render( Select All Production AWS Development Azure , ); await user.click(screen.getByRole("combobox")); await user.click(screen.getByRole("button", { name: /select all/i })); expect(onValuesChange).toHaveBeenCalledWith(["aws-prod", "azure-dev"]); }); it("does not select disabled options", async () => { const user = userEvent.setup(); const onValuesChange = vi.fn(); render( Production AWS Disconnected AWS , ); await user.click(screen.getByRole("combobox")); const disabledOption = screen.getByRole("option", { name: /disconnected aws/i, }); expect(disabledOption).toHaveAttribute("data-disabled", "true"); expect(disabledOption).toHaveAttribute("aria-disabled", "true"); await user.click(disabledOption); expect(onValuesChange).not.toHaveBeenCalled(); }); });