carbon-components-svelte/tests/MultiSelect/MultiSelect.test.ts
2025-03-20 17:34:20 -07:00

351 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { render, screen } from "@testing-library/svelte";
import { MultiSelect } from "carbon-components-svelte";
import { user } from "../setup-tests";
const items = [
{ id: "0", text: "Slack" },
{ id: "1", text: "Email" },
{ id: "2", text: "Fax" },
] as const;
describe("MultiSelect", () => {
/** Opens the dropdown. */
const openMenu = async () =>
await user.click(
await screen.findByLabelText("Open menu", {
selector: `[role="button"]`,
}),
);
/** Closes the dropdown. */
const closeMenu = async () =>
await user.click(
await screen.findByLabelText("Close menu", {
selector: `[role="button"]`,
}),
);
/** Toggles an option, identifying it by its `text` value. */
const toggleOption = async (optionText: string) =>
await user.click(
await screen.findByText((text) => text.trim() === optionText),
);
/** Fetches the `text` value of the nth option in the MultiSelect component. */
const nthRenderedOptionText = (index: number) =>
screen.queryAllByRole("option").at(index)?.textContent?.trim();
describe("sorting behavior", () => {
it("initially sorts items alphabetically", async () => {
render(MultiSelect, {
items: [
{ id: "1", text: "C" },
{ id: "3", text: "A" },
{ id: "2", text: "B" },
],
});
// Initially, items should be sorted alphabetically.
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A");
expect(nthRenderedOptionText(1)).toBe("B");
expect(nthRenderedOptionText(2)).toBe("C");
});
it("immediately moves selected items to the top (with selectionFeedback: top)", async () => {
render(MultiSelect, {
items: [
{ id: "3", text: "C" },
{ id: "1", text: "A" },
{ id: "2", text: "B" },
],
selectionFeedback: "top",
});
// Initially, items should be sorted alphabetically.
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A");
await toggleOption("C");
expect(nthRenderedOptionText(0)).toBe("C");
await toggleOption("C");
expect(nthRenderedOptionText(0)).toBe("A");
});
it("sorts newly-toggled items only after the dropdown is reoponed (with selectionFeedback: top-after-reopen)", async () => {
render(MultiSelect, {
items: [
{ id: "3", text: "C" },
{ id: "1", text: "A" },
{ id: "2", text: "B" },
],
});
// Initially, items should be sorted alphabetically.
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A");
// While the menu is still open, a newly-selected item should not move.
await toggleOption("C");
expect(nthRenderedOptionText(0)).toBe("A");
// The newly-selected item should move after the menu is closed and
// re-opened.
await closeMenu();
await openMenu();
expect(nthRenderedOptionText(0)).toBe("C");
// A deselected item should not move while the dropdown is still open.
await toggleOption("C");
expect(nthRenderedOptionText(0)).toBe("C");
// The deselected item should move after closing and re-opening the dropdown.
await closeMenu();
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A");
});
it("never moves selected items to the top (with selectionFeedback: fixed)", async () => {
render(MultiSelect, {
items: [
{ id: "3", text: "C" },
{ id: "1", text: "A" },
{ id: "2", text: "B" },
],
selectionFeedback: "fixed",
});
// Items should be sorted alphabetically.
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A");
// A newly-selected item should not move after the selection is made.
await toggleOption("C");
expect(nthRenderedOptionText(0)).toBe("A");
// The newly-selected item also shouldnt move after the dropdown is closed
// and reopened.
await closeMenu();
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A");
});
});
describe("filtering behavior", () => {
it("should filter items based on input value", async () => {
render(MultiSelect, {
items,
filterable: true,
placeholder: "Filter items...",
});
await openMenu();
const input = screen.getByPlaceholderText("Filter items...");
await user.type(input, "em");
expect(screen.queryByText("Slack")).not.toBeInTheDocument();
expect(screen.getByText("Email")).toBeInTheDocument();
expect(screen.queryByText("Fax")).not.toBeInTheDocument();
});
it("should use custom filter function", async () => {
render(MultiSelect, {
items,
filterable: true,
filterItem: (item, value) =>
item.text.toLowerCase().startsWith(value.toLowerCase()),
});
await openMenu();
const input = screen.getByRole("combobox");
await user.type(input, "e");
expect(screen.queryByText("Slack")).not.toBeInTheDocument();
expect(screen.getByText("Email")).toBeInTheDocument();
expect(screen.queryByText("Fax")).not.toBeInTheDocument();
});
// TODO(bug): ListBoxSelection aria-labels should be user-friendly
it.skip("should clear filter on selection clear", async () => {
render(MultiSelect, {
items,
filterable: true,
selectedIds: ["0"],
});
const clearButton = screen.getByLabelText("Clear all");
await user.click(clearButton);
const input = screen.getByRole("combobox");
expect(input).toHaveValue("");
});
});
describe("keyboard navigation", () => {
it("should handle arrow keys for navigation", async () => {
render(MultiSelect, { items });
await openMenu();
await user.keyboard("{ArrowDown}");
const options = screen.getAllByRole("option");
expect(options[0]).toHaveClass("bx--list-box__menu-item--highlighted");
});
it("should select item with Enter key", async () => {
const { component } = render(MultiSelect, { items });
const selectHandler = vi.fn();
component.$on("select", selectHandler);
await openMenu();
await user.keyboard("{ArrowDown}");
await user.keyboard("{Enter}");
expect(selectHandler).toHaveBeenCalled();
});
it("should close menu with Escape key", async () => {
render(MultiSelect, { items });
await openMenu();
await user.keyboard("{Escape}");
const button = screen.getByRole("button");
expect(button).toHaveAttribute("aria-expanded", "false");
});
});
describe("accessibility", () => {
it("should handle hidden label", () => {
render(MultiSelect, {
items,
titleText: "Contact methods",
hideLabel: true,
});
const label = screen.getByText("Contact methods");
expect(label).toHaveClass("bx--visually-hidden");
});
it("should handle custom aria-label", async () => {
render(MultiSelect, {
items,
"aria-label": "Custom label",
});
await openMenu();
const menu = screen.getByLabelText("Custom label");
expect(menu).toBeInTheDocument();
});
});
describe("variants and states", () => {
it("should render in light variant", async () => {
render(MultiSelect, {
items,
light: true,
});
await openMenu();
const listBox = screen.getByRole("listbox").closest(".bx--list-box");
expect(listBox).toHaveClass("bx--list-box--light");
});
it("should render in inline variant", () => {
render(MultiSelect, {
items,
type: "inline",
});
const wrapper = screen
.getByRole("button")
.closest(".bx--multi-select__wrapper");
expect(wrapper).toHaveClass("bx--multi-select__wrapper--inline");
});
it("should handle invalid state", () => {
render(MultiSelect, {
items,
invalid: true,
invalidText: "Invalid selection",
});
expect(screen.getByText("Invalid selection")).toBeInTheDocument();
const wrapper = screen.getByRole("button").closest(".bx--list-box");
expect(wrapper).toHaveClass("bx--multi-select--invalid");
});
it("should handle warning state", () => {
render(MultiSelect, {
items,
warn: true,
warnText: "Warning message",
});
expect(screen.getByText("Warning message")).toBeInTheDocument();
const wrapper = screen.getByRole("button").closest(".bx--list-box");
expect(wrapper).toHaveClass("bx--list-box--warning");
});
it("should handle disabled state", () => {
render(MultiSelect, { items, disabled: true });
const field = screen.getByRole("button");
expect(field).toHaveAttribute("aria-disabled", "true");
expect(field).toHaveAttribute("tabindex", "-1");
expect(field.closest(".bx--multi-select")).toHaveAttribute(
"tabindex",
"-1",
);
});
it("should handle disabled items", async () => {
const itemsWithDisabled = [
{ id: "0", text: "Slack" },
{ id: "1", text: "Email", disabled: true },
{ id: "2", text: "Fax" },
];
render(MultiSelect, {
items: itemsWithDisabled,
});
await openMenu();
const emailOption = screen
.getByText("Email")
.closest(".bx--list-box__menu-item");
expect(emailOption).toHaveAttribute("disabled");
});
});
describe("custom formatting", () => {
it("should handle custom itemToString", () => {
render(MultiSelect, {
items,
selectedIds: ["0"],
itemToString: (item) => `${item.text} (${item.id})`,
});
expect(screen.getByText("Slack (0)")).toBeInTheDocument();
});
it("should handle custom itemToInput", async () => {
render(MultiSelect, {
items,
itemToInput: (item) => ({
name: `contact_${item.id}`,
value: item.text.toLowerCase(),
}),
});
await openMenu();
const checkbox = screen.getByText("Slack");
const checkboxWrapper = checkbox.closest(".bx--checkbox-wrapper");
assert(checkboxWrapper);
const checkboxInput = checkboxWrapper.querySelector("input");
expect(checkboxInput).toHaveAttribute("name", "contact_0");
});
});
});