mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
387 lines
12 KiB
TypeScript
387 lines
12 KiB
TypeScript
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(
|
|
<MultiSelect values={["aws-prod"]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
// 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(
|
|
<MultiSelect values={["FAIL"]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="All Status" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectItem value="FAIL">FAIL</MultiSelectItem>
|
|
<MultiSelectItem value="PASS">PASS</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<MultiSelect values={[]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent
|
|
search={{
|
|
placeholder: "Search accounts...",
|
|
emptyMessage: "No accounts found.",
|
|
}}
|
|
>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<MultiSelect values={[]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent
|
|
search={{
|
|
placeholder: "Search accounts...",
|
|
emptyMessage: "No accounts found.",
|
|
}}
|
|
>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<MultiSelect values={[]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent
|
|
search={{
|
|
placeholder: "Search accounts...",
|
|
emptyMessage: "No accounts found.",
|
|
}}
|
|
>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<div>
|
|
<MultiSelect values={[]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent
|
|
search={{
|
|
placeholder: "Search accounts...",
|
|
emptyMessage: "No accounts found.",
|
|
}}
|
|
>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>
|
|
<button type="button">Outside target</button>
|
|
</div>,
|
|
);
|
|
|
|
// 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("sizes the dropdown to its content with a capped width", async () => {
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<MultiSelect values={[]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectItem value="very-long-item">
|
|
This is a very long option label that should not expand the dropdown
|
|
indefinitely
|
|
</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
await user.click(screen.getByRole("combobox"));
|
|
|
|
expect(screen.getByRole("dialog")).toHaveClass("sm:w-max");
|
|
expect(screen.getByRole("dialog")).toHaveClass("sm:max-w-[22rem]");
|
|
});
|
|
|
|
it("keeps long option lists scrollable inside the dropdown", async () => {
|
|
// Given
|
|
const user = userEvent.setup();
|
|
|
|
render(
|
|
<MultiSelect values={[]} onValuesChange={() => {}}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
{Array.from({ length: 20 }, (_, index) => (
|
|
<MultiSelectItem key={index} value={`account-${index}`}>
|
|
Account {index}
|
|
</MultiSelectItem>
|
|
))}
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
// 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(
|
|
<MultiSelect values={["aws-prod"]} onValuesChange={onValuesChange}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectSelectAll>Select All</MultiSelectSelectAll>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<MultiSelect values={[]} onValuesChange={onValuesChange}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectSelectAll>Select All</MultiSelectSelectAll>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<MultiSelect values={[]} onValuesChange={onValuesChange}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectSelectAll
|
|
mode="select"
|
|
values={["aws-prod", "azure-dev"]}
|
|
>
|
|
Select All
|
|
</MultiSelectSelectAll>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="azure-dev">Development Azure</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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(
|
|
<MultiSelect values={[]} onValuesChange={onValuesChange}>
|
|
<MultiSelectTrigger>
|
|
<MultiSelectValue placeholder="Select accounts" />
|
|
</MultiSelectTrigger>
|
|
<MultiSelectContent search={false}>
|
|
<MultiSelectItem value="aws-prod">Production AWS</MultiSelectItem>
|
|
<MultiSelectItem value="aws-disconnected" disabled>
|
|
Disconnected AWS
|
|
</MultiSelectItem>
|
|
</MultiSelectContent>
|
|
</MultiSelect>,
|
|
);
|
|
|
|
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();
|
|
});
|
|
});
|