test(multi-select): more unit tests

This commit is contained in:
Eric Liu 2025-03-20 16:42:13 -07:00
commit f200dadb97

View file

@ -2,7 +2,13 @@ import { render, screen } from "@testing-library/svelte";
import { MultiSelect } from "carbon-components-svelte"; import { MultiSelect } from "carbon-components-svelte";
import { user } from "../setup-tests"; import { user } from "../setup-tests";
describe("MultiSelect sorts items correctly", () => { const items = [
{ id: "0", text: "Slack" },
{ id: "1", text: "Email" },
{ id: "2", text: "Fax" },
] as const;
describe("MultiSelect", () => {
/** Opens the dropdown. */ /** Opens the dropdown. */
const openMenu = async () => const openMenu = async () =>
await user.click( await user.click(
@ -29,96 +35,317 @@ describe("MultiSelect sorts items correctly", () => {
const nthRenderedOptionText = (index: number) => const nthRenderedOptionText = (index: number) =>
screen.queryAllByRole("option").at(index)?.textContent?.trim(); screen.queryAllByRole("option").at(index)?.textContent?.trim();
it("initially sorts items alphabetically", async () => { describe("sorting behavior", () => {
render(MultiSelect, { it("initially sorts items alphabetically", async () => {
items: [ render(MultiSelect, {
{ id: "1", text: "C" }, items: [
{ id: "3", text: "A" }, { id: "1", text: "C" },
{ id: "2", text: "B" }, { 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");
}); });
await openMenu(); it("immediately moves selected items to the top (with selectionFeedback: top)", async () => {
expect(nthRenderedOptionText(0)).toBe("A"); render(MultiSelect, {
expect(nthRenderedOptionText(1)).toBe("B"); items: [
expect(nthRenderedOptionText(2)).toBe("C"); { 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");
});
}); });
it("immediately moves selected items to the top (with selectionFeedback: top)", async () => { describe("filtering behavior", () => {
render(MultiSelect, { it("should filter items based on input value", async () => {
items: [ render(MultiSelect, {
{ id: "3", text: "C" }, items,
{ id: "1", text: "A" }, filterable: true,
{ id: "2", text: "B" }, placeholder: "Filter items...",
], });
selectionFeedback: "top",
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();
}); });
await openMenu(); it("should use custom filter function", async () => {
expect(nthRenderedOptionText(0)).toBe("A"); render(MultiSelect, {
items,
filterable: true,
filterItem: (item, value) =>
item.text.toLowerCase().startsWith(value.toLowerCase()),
});
await toggleOption("C"); await openMenu();
expect(nthRenderedOptionText(0)).toBe("C"); const input = screen.getByRole("combobox");
await user.type(input, "e");
await toggleOption("C"); expect(screen.queryByText("Slack")).not.toBeInTheDocument();
expect(nthRenderedOptionText(0)).toBe("A"); 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("");
});
}); });
it("sorts newly-toggled items only after the dropdown is reoponed (with selectionFeedback: top-after-reopen)", async () => { describe("keyboard navigation", () => {
render(MultiSelect, { it("should handle arrow keys for navigation", async () => {
items: [ render(MultiSelect, { items });
{ id: "3", text: "C" },
{ id: "1", text: "A" }, await openMenu();
{ id: "2", text: "B" }, await user.keyboard("{ArrowDown}");
],
const options = screen.getAllByRole("option");
expect(options[0]).toHaveClass("bx--list-box__menu-item--highlighted");
}); });
// Initially, items should be sorted alphabetically. it("should select item with Enter key", async () => {
await openMenu(); const { component } = render(MultiSelect, { items });
expect(nthRenderedOptionText(0)).toBe("A"); const selectHandler = vi.fn();
component.$on("select", selectHandler);
// While the menu is still open, a newly-selected item should not move. await openMenu();
await toggleOption("C"); await user.keyboard("{ArrowDown}");
expect(nthRenderedOptionText(0)).toBe("A"); await user.keyboard("{Enter}");
// The newly-selected item should move after the menu is closed and expect(selectHandler).toHaveBeenCalled();
// re-opened. });
await closeMenu();
await openMenu();
expect(nthRenderedOptionText(0)).toBe("C");
// A deselected item should not move while the dropdown is still open. it("should close menu with Escape key", async () => {
await toggleOption("C"); render(MultiSelect, { items });
expect(nthRenderedOptionText(0)).toBe("C");
// The deselected item should move after closing and re-opening the dropdown. await openMenu();
await closeMenu(); await user.keyboard("{Escape}");
await openMenu();
expect(nthRenderedOptionText(0)).toBe("A"); const button = screen.getByRole("button");
expect(button).toHaveAttribute("aria-expanded", "false");
});
}); });
it("never moves selected items to the top (with selectionFeedback: fixed)", async () => { describe("accessibility", () => {
render(MultiSelect, { it("should handle hidden label", () => {
items: [ render(MultiSelect, {
{ id: "3", text: "C" }, items,
{ id: "1", text: "A" }, titleText: "Contact methods",
{ id: "2", text: "B" }, hideLabel: true,
], });
selectionFeedback: "fixed",
const label = screen.getByText("Contact methods");
expect(label).toHaveClass("bx--visually-hidden");
}); });
// Items should be sorted alphabetically. it("should handle custom aria-label", async () => {
await openMenu(); render(MultiSelect, {
expect(nthRenderedOptionText(0)).toBe("A"); items,
"aria-label": "Custom label",
});
// A newly-selected item should not move after the selection is made. await openMenu();
await toggleOption("C"); const menu = screen.getByLabelText("Custom label");
expect(nthRenderedOptionText(0)).toBe("A"); expect(menu).toBeInTheDocument();
});
});
// The newly-selected item also shouldnt move after the dropdown is closed describe("variants and states", () => {
// and reopened. it("should render in light variant", async () => {
await closeMenu(); render(MultiSelect, {
await openMenu(); items,
expect(nthRenderedOptionText(0)).toBe("A"); 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");
});
}); });
}); });