mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-14 18:01:06 +00:00
test(multi-select): more unit tests
This commit is contained in:
parent
5e8c8b983c
commit
8ab77c99e2
4 changed files with 328 additions and 360 deletions
|
@ -1,140 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { MultiSelect } from "carbon-components-svelte";
|
|
||||||
import type { MultiSelectProps } from "carbon-components-svelte/MultiSelect/MultiSelect.svelte";
|
|
||||||
|
|
||||||
let selectedIds: MultiSelectProps["selectedIds"] = [0];
|
|
||||||
|
|
||||||
$: {
|
|
||||||
// @ts-expect-error
|
|
||||||
selectedIds[0] = [0];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
direction="top"
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
hideLabel
|
|
||||||
bind:selectedIds
|
|
||||||
items={[
|
|
||||||
{ id: 0, text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax", disabled: true },
|
|
||||||
]}
|
|
||||||
on:select={(e) => {
|
|
||||||
console.log(e.detail.selectedIds);
|
|
||||||
console.log(e.detail.selected);
|
|
||||||
console.log(e.detail.unselected);
|
|
||||||
}}
|
|
||||||
on:blur={(e) => {
|
|
||||||
e.detail; // number | FocusEvent
|
|
||||||
}}
|
|
||||||
on:paste
|
|
||||||
translateWithId={(id) => {
|
|
||||||
console.log(id); // "open" | "close"
|
|
||||||
return id;
|
|
||||||
}}
|
|
||||||
translateWithIdSelection={(id) => {
|
|
||||||
console.log(id); // "clearAll" | "clearSelection"
|
|
||||||
return id;
|
|
||||||
}}
|
|
||||||
let:item
|
|
||||||
let:index
|
|
||||||
>
|
|
||||||
{item.id}
|
|
||||||
{index}
|
|
||||||
</MultiSelect>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
sortItem={() => {}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
selectedIds={["0", "1"]}
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
itemToString={(item) => {
|
|
||||||
return item.text + " (" + item.id + ")";
|
|
||||||
}}
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
sortItem={() => {}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
light
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
type="inline"
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
size="xl"
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
size="sm"
|
|
||||||
titleText="Contact"
|
|
||||||
label="Select contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MultiSelect
|
|
||||||
filterable
|
|
||||||
filterItem={(item, query) => {
|
|
||||||
return item.text.toLowerCase().includes(query.toLowerCase());
|
|
||||||
}}
|
|
||||||
titleText="Contact"
|
|
||||||
placeholder="Filter contact methods..."
|
|
||||||
items={[
|
|
||||||
{ id: "0", text: "Slack" },
|
|
||||||
{ id: "1", text: "Email" },
|
|
||||||
{ id: "2", text: "Fax" },
|
|
||||||
]}
|
|
||||||
/>
|
|
55
tests/MultiSelect/MultiSelect.test.svelte
Normal file
55
tests/MultiSelect/MultiSelect.test.svelte
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { MultiSelect } from "carbon-components-svelte";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
export let items: ComponentProps<MultiSelect>["items"] = [];
|
||||||
|
export let selectedIds: ComponentProps<MultiSelect>["selectedIds"] = [];
|
||||||
|
export let filterable = false;
|
||||||
|
export let filterItem: ComponentProps<MultiSelect>["filterItem"] = undefined;
|
||||||
|
export let placeholder = "";
|
||||||
|
export let titleText = "";
|
||||||
|
export let hideLabel = false;
|
||||||
|
export let light = false;
|
||||||
|
export let type: ComponentProps<MultiSelect>["type"] = "default";
|
||||||
|
export let invalid = false;
|
||||||
|
export let invalidText = "";
|
||||||
|
export let warn = false;
|
||||||
|
export let warnText = "";
|
||||||
|
export let disabled = false;
|
||||||
|
export let selectionFeedback: ComponentProps<MultiSelect>["selectionFeedback"] =
|
||||||
|
"top-after-reopen";
|
||||||
|
export let translateWithIdSelection: ComponentProps<MultiSelect>["translateWithIdSelection"] =
|
||||||
|
undefined;
|
||||||
|
export let itemToString: ComponentProps<MultiSelect>["itemToString"] = (
|
||||||
|
item,
|
||||||
|
) => item.text;
|
||||||
|
export let itemToInput: ComponentProps<MultiSelect>["itemToInput"] =
|
||||||
|
undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<MultiSelect
|
||||||
|
{items}
|
||||||
|
{selectedIds}
|
||||||
|
{filterable}
|
||||||
|
{filterItem}
|
||||||
|
{placeholder}
|
||||||
|
{titleText}
|
||||||
|
{hideLabel}
|
||||||
|
{light}
|
||||||
|
{type}
|
||||||
|
{invalid}
|
||||||
|
{invalidText}
|
||||||
|
{warn}
|
||||||
|
{warnText}
|
||||||
|
{disabled}
|
||||||
|
{selectionFeedback}
|
||||||
|
{translateWithIdSelection}
|
||||||
|
{itemToString}
|
||||||
|
{itemToInput}
|
||||||
|
on:select={(e) => {
|
||||||
|
console.log("select", e.detail);
|
||||||
|
}}
|
||||||
|
on:clear={(e) => {
|
||||||
|
console.log("clear", e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
|
@ -1,6 +1,7 @@
|
||||||
import { render, screen } from "@testing-library/svelte";
|
import { render, screen } from "@testing-library/svelte";
|
||||||
import { MultiSelect } from "carbon-components-svelte";
|
|
||||||
import { user } from "../setup-tests";
|
import { user } from "../setup-tests";
|
||||||
|
import MultiSelect from "./MultiSelect.test.svelte";
|
||||||
|
import MultiSelectSlot from "./MultiSelectSlot.test.svelte";
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ id: "0", text: "Slack" },
|
{ id: "0", text: "Slack" },
|
||||||
|
@ -9,7 +10,6 @@ const items = [
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
describe("MultiSelect", () => {
|
describe("MultiSelect", () => {
|
||||||
/** Opens the dropdown. */
|
|
||||||
const openMenu = async () =>
|
const openMenu = async () =>
|
||||||
await user.click(
|
await user.click(
|
||||||
await screen.findByLabelText("Open menu", {
|
await screen.findByLabelText("Open menu", {
|
||||||
|
@ -17,7 +17,6 @@ describe("MultiSelect", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Closes the dropdown. */
|
|
||||||
const closeMenu = async () =>
|
const closeMenu = async () =>
|
||||||
await user.click(
|
await user.click(
|
||||||
await screen.findByLabelText("Close menu", {
|
await screen.findByLabelText("Close menu", {
|
||||||
|
@ -25,119 +24,126 @@ describe("MultiSelect", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Toggles an option, identifying it by its `text` value. */
|
|
||||||
const toggleOption = async (optionText: string) =>
|
const toggleOption = async (optionText: string) =>
|
||||||
await user.click(
|
await user.click(
|
||||||
await screen.findByText((text) => text.trim() === optionText),
|
await screen.findByText((text) => text.trim() === optionText),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Fetches the `text` value of the nth option in the MultiSelect component. */
|
|
||||||
const nthRenderedOptionText = (index: number) =>
|
const nthRenderedOptionText = (index: number) =>
|
||||||
screen.queryAllByRole("option").at(index)?.textContent?.trim();
|
screen.queryAllByRole("option").at(index)?.textContent?.trim();
|
||||||
|
|
||||||
describe("sorting behavior", () => {
|
it("renders with default props", () => {
|
||||||
it("initially sorts items alphabetically", async () => {
|
render(MultiSelect, {
|
||||||
render(MultiSelect, {
|
props: {
|
||||||
items: [
|
items,
|
||||||
{ id: "1", text: "C" },
|
titleText: "Contact methods",
|
||||||
{ 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 () => {
|
expect(screen.getByText("Contact methods")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button")).toHaveAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders default slot", () => {
|
||||||
|
render(MultiSelectSlot, { items });
|
||||||
|
expect(screen.getByText("1 Email 0")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("2 Fax 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("0 Slack 2")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("selection behavior", () => {
|
||||||
|
it("handles item selection", async () => {
|
||||||
|
const consoleLog = vi.spyOn(console, "log");
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items: [
|
props: { items },
|
||||||
{ id: "3", text: "C" },
|
|
||||||
{ id: "1", text: "A" },
|
|
||||||
{ id: "2", text: "B" },
|
|
||||||
],
|
|
||||||
selectionFeedback: "top",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initially, items should be sorted alphabetically.
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
await toggleOption("Slack");
|
||||||
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
await toggleOption("C");
|
selectedIds: ["0"],
|
||||||
expect(nthRenderedOptionText(0)).toBe("C");
|
selected: [{ id: "0", text: "Slack", checked: true }],
|
||||||
|
unselected: [
|
||||||
await toggleOption("C");
|
{ id: "1", text: "Email", checked: false },
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
{ id: "2", text: "Fax", checked: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sorts newly-toggled items only after the dropdown is reoponed (with selectionFeedback: top-after-reopen)", async () => {
|
it("handles multiple selections", async () => {
|
||||||
|
const consoleLog = vi.spyOn(console, "log");
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items: [
|
props: { items },
|
||||||
{ id: "3", text: "C" },
|
|
||||||
{ id: "1", text: "A" },
|
|
||||||
{ id: "2", text: "B" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initially, items should be sorted alphabetically.
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
await toggleOption("Slack");
|
||||||
|
await toggleOption("Email");
|
||||||
// While the menu is still open, a newly-selected item should not move.
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
await toggleOption("C");
|
selectedIds: ["0"],
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
selected: [{ id: "0", text: "Slack", checked: true }],
|
||||||
|
unselected: [
|
||||||
// The newly-selected item should move after the menu is closed and
|
{ id: "1", text: "Email", checked: false },
|
||||||
// re-opened.
|
{ id: "2", text: "Fax", checked: false },
|
||||||
await closeMenu();
|
],
|
||||||
await openMenu();
|
});
|
||||||
expect(nthRenderedOptionText(0)).toBe("C");
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
|
selectedIds: ["1", "0"],
|
||||||
// A deselected item should not move while the dropdown is still open.
|
selected: [
|
||||||
await toggleOption("C");
|
{ id: "1", text: "Email", checked: true },
|
||||||
expect(nthRenderedOptionText(0)).toBe("C");
|
{ id: "0", text: "Slack", checked: true },
|
||||||
|
],
|
||||||
// The deselected item should move after closing and re-opening the dropdown.
|
unselected: [{ id: "2", text: "Fax", checked: false }],
|
||||||
await closeMenu();
|
});
|
||||||
await openMenu();
|
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never moves selected items to the top (with selectionFeedback: fixed)", async () => {
|
it("handles item deselection", async () => {
|
||||||
|
const consoleLog = vi.spyOn(console, "log");
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items: [
|
props: {
|
||||||
{ id: "3", text: "C" },
|
items,
|
||||||
{ id: "1", text: "A" },
|
selectedIds: ["0"],
|
||||||
{ id: "2", text: "B" },
|
},
|
||||||
],
|
|
||||||
selectionFeedback: "fixed",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Items should be sorted alphabetically.
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
await toggleOption("Slack");
|
||||||
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
|
selectedIds: [],
|
||||||
|
selected: [],
|
||||||
|
unselected: [
|
||||||
|
{ id: "0", text: "Slack", checked: false },
|
||||||
|
{ id: "1", text: "Email", checked: false },
|
||||||
|
{ id: "2", text: "Fax", checked: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
// A newly-selected item should not move after the selection is made.
|
await toggleOption("Slack");
|
||||||
await toggleOption("C");
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
selectedIds: [],
|
||||||
|
selected: [],
|
||||||
// The newly-selected item also shouldn't move after the dropdown is closed
|
unselected: [
|
||||||
// and reopened.
|
{ id: "0", text: "Slack", checked: false },
|
||||||
await closeMenu();
|
{ id: "1", text: "Email", checked: false },
|
||||||
await openMenu();
|
{ id: "2", text: "Fax", checked: false },
|
||||||
expect(nthRenderedOptionText(0)).toBe("A");
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("filtering behavior", () => {
|
describe("filtering behavior", () => {
|
||||||
it("should filter items based on input value", async () => {
|
it("filters items based on input", async () => {
|
||||||
|
const consoleLog = vi.spyOn(console, "log");
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
filterable: true,
|
items,
|
||||||
placeholder: "Filter items...",
|
filterable: true,
|
||||||
|
placeholder: "Filter items...",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
|
@ -147,14 +153,28 @@ describe("MultiSelect", () => {
|
||||||
expect(screen.queryByText("Slack")).not.toBeInTheDocument();
|
expect(screen.queryByText("Slack")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("Email")).toBeInTheDocument();
|
expect(screen.getByText("Email")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("Fax")).not.toBeInTheDocument();
|
expect(screen.queryByText("Fax")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.keyboard("{ArrowDown}{Enter}");
|
||||||
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
|
selectedIds: ["1"],
|
||||||
|
selected: [{ id: "1", text: "Email", checked: true }],
|
||||||
|
unselected: [
|
||||||
|
{ id: "2", text: "Fax", checked: false },
|
||||||
|
{ id: "0", text: "Slack", checked: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should use custom filter function", async () => {
|
it("uses custom filter function", async () => {
|
||||||
|
const consoleLog = vi.spyOn(console, "log");
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
filterable: true,
|
items,
|
||||||
filterItem: (item, value) =>
|
filterable: true,
|
||||||
item.text.toLowerCase().startsWith(value.toLowerCase()),
|
filterItem: (item, value) => {
|
||||||
|
return item.text.toLowerCase().startsWith(value.toLowerCase());
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
|
@ -164,122 +184,112 @@ describe("MultiSelect", () => {
|
||||||
expect(screen.queryByText("Slack")).not.toBeInTheDocument();
|
expect(screen.queryByText("Slack")).not.toBeInTheDocument();
|
||||||
expect(screen.getByText("Email")).toBeInTheDocument();
|
expect(screen.getByText("Email")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("Fax")).not.toBeInTheDocument();
|
expect(screen.queryByText("Fax")).not.toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
it("should clear filter on selection clear", async () => {
|
await user.keyboard("{ArrowDown}{Enter}");
|
||||||
render(MultiSelect, {
|
expect(consoleLog).toHaveBeenCalledWith("select", {
|
||||||
items,
|
selectedIds: ["1"],
|
||||||
filterable: true,
|
selected: [{ id: "1", text: "Email", checked: true }],
|
||||||
selectedIds: ["0"],
|
unselected: [
|
||||||
|
{ id: "2", text: "Fax", checked: false },
|
||||||
|
{ id: "0", text: "Slack", checked: false },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearButton = screen.getByLabelText("Clear all selected items");
|
|
||||||
await user.click(clearButton);
|
|
||||||
|
|
||||||
const input = screen.getByRole("combobox");
|
|
||||||
expect(input).toHaveValue("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show correct clear button label regardless of selection count", async () => {
|
|
||||||
render(MultiSelect, {
|
|
||||||
items,
|
|
||||||
selectedIds: ["0"],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(
|
|
||||||
screen.getByLabelText("Clear all selected items"),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
await openMenu();
|
|
||||||
await toggleOption("Email");
|
|
||||||
expect(
|
|
||||||
screen.getByLabelText("Clear all selected items"),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should use custom translations when translateWithId is provided", async () => {
|
|
||||||
const customTranslations = {
|
|
||||||
clearSelection: "Remove selected item",
|
|
||||||
clearAll: "Remove all items",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
render(MultiSelect, {
|
|
||||||
items,
|
|
||||||
selectedIds: ["0"],
|
|
||||||
translateWithIdSelection: (id) => customTranslations[id],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(screen.getByLabelText("Remove all items")).toBeInTheDocument();
|
|
||||||
|
|
||||||
await openMenu();
|
|
||||||
await toggleOption("Email");
|
|
||||||
expect(screen.getByLabelText("Remove all items")).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("keyboard navigation", () => {
|
describe("sorting behavior", () => {
|
||||||
it("should handle arrow keys for navigation", async () => {
|
it("initially sorts items alphabetically", 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, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
titleText: "Contact methods",
|
items: [
|
||||||
hideLabel: true,
|
{ id: "1", text: "C" },
|
||||||
});
|
{ id: "3", text: "A" },
|
||||||
|
{ id: "2", text: "B" },
|
||||||
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();
|
await openMenu();
|
||||||
const menu = screen.getByLabelText("Custom label");
|
expect(nthRenderedOptionText(0)).toBe("A");
|
||||||
expect(menu).toBeInTheDocument();
|
expect(nthRenderedOptionText(1)).toBe("B");
|
||||||
|
expect(nthRenderedOptionText(2)).toBe("C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves selected items to top with selectionFeedback: top", async () => {
|
||||||
|
render(MultiSelect, {
|
||||||
|
props: {
|
||||||
|
items: [
|
||||||
|
{ id: "3", text: "C" },
|
||||||
|
{ id: "1", text: "A" },
|
||||||
|
{ id: "2", text: "B" },
|
||||||
|
],
|
||||||
|
selectionFeedback: "top",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 after reopen with selectionFeedback: top-after-reopen", async () => {
|
||||||
|
render(MultiSelect, {
|
||||||
|
props: {
|
||||||
|
items: [
|
||||||
|
{ id: "3", text: "C" },
|
||||||
|
{ id: "1", text: "A" },
|
||||||
|
{ id: "2", text: "B" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await openMenu();
|
||||||
|
expect(nthRenderedOptionText(0)).toBe("A");
|
||||||
|
|
||||||
|
await toggleOption("C");
|
||||||
|
expect(nthRenderedOptionText(0)).toBe("A");
|
||||||
|
|
||||||
|
await closeMenu();
|
||||||
|
await openMenu();
|
||||||
|
expect(nthRenderedOptionText(0)).toBe("C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maintains order with selectionFeedback: fixed", async () => {
|
||||||
|
render(MultiSelect, {
|
||||||
|
props: {
|
||||||
|
items: [
|
||||||
|
{ id: "3", text: "C" },
|
||||||
|
{ id: "1", text: "A" },
|
||||||
|
{ id: "2", text: "B" },
|
||||||
|
],
|
||||||
|
selectionFeedback: "fixed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await openMenu();
|
||||||
|
expect(nthRenderedOptionText(0)).toBe("A");
|
||||||
|
|
||||||
|
await toggleOption("C");
|
||||||
|
expect(nthRenderedOptionText(0)).toBe("A");
|
||||||
|
|
||||||
|
await closeMenu();
|
||||||
|
await openMenu();
|
||||||
|
expect(nthRenderedOptionText(0)).toBe("A");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("variants and states", () => {
|
describe("variants and states", () => {
|
||||||
it("should render in light variant", async () => {
|
it("renders in light variant", async () => {
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
light: true,
|
items,
|
||||||
|
light: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
|
@ -287,10 +297,12 @@ describe("MultiSelect", () => {
|
||||||
expect(listBox).toHaveClass("bx--list-box--light");
|
expect(listBox).toHaveClass("bx--list-box--light");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render in inline variant", () => {
|
it("renders in inline variant", () => {
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
type: "inline",
|
items,
|
||||||
|
type: "inline",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = screen
|
const wrapper = screen
|
||||||
|
@ -299,11 +311,13 @@ describe("MultiSelect", () => {
|
||||||
expect(wrapper).toHaveClass("bx--multi-select__wrapper--inline");
|
expect(wrapper).toHaveClass("bx--multi-select__wrapper--inline");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle invalid state", () => {
|
it("handles invalid state", () => {
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
invalid: true,
|
items,
|
||||||
invalidText: "Invalid selection",
|
invalid: true,
|
||||||
|
invalidText: "Invalid selection",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Invalid selection")).toBeInTheDocument();
|
expect(screen.getByText("Invalid selection")).toBeInTheDocument();
|
||||||
|
@ -311,11 +325,13 @@ describe("MultiSelect", () => {
|
||||||
expect(wrapper).toHaveClass("bx--multi-select--invalid");
|
expect(wrapper).toHaveClass("bx--multi-select--invalid");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle warning state", () => {
|
it("handles warning state", () => {
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
warn: true,
|
items,
|
||||||
warnText: "Warning message",
|
warn: true,
|
||||||
|
warnText: "Warning message",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Warning message")).toBeInTheDocument();
|
expect(screen.getByText("Warning message")).toBeInTheDocument();
|
||||||
|
@ -323,8 +339,13 @@ describe("MultiSelect", () => {
|
||||||
expect(wrapper).toHaveClass("bx--list-box--warning");
|
expect(wrapper).toHaveClass("bx--list-box--warning");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle disabled state", () => {
|
it("handles disabled state", () => {
|
||||||
render(MultiSelect, { items, disabled: true });
|
render(MultiSelect, {
|
||||||
|
props: {
|
||||||
|
items,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const field = screen.getByRole("button");
|
const field = screen.getByRole("button");
|
||||||
expect(field).toHaveAttribute("aria-disabled", "true");
|
expect(field).toHaveAttribute("aria-disabled", "true");
|
||||||
|
@ -335,7 +356,7 @@ describe("MultiSelect", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle disabled items", async () => {
|
it("handles disabled items", async () => {
|
||||||
const itemsWithDisabled = [
|
const itemsWithDisabled = [
|
||||||
{ id: "0", text: "Slack" },
|
{ id: "0", text: "Slack" },
|
||||||
{ id: "1", text: "Email", disabled: true },
|
{ id: "1", text: "Email", disabled: true },
|
||||||
|
@ -343,7 +364,9 @@ describe("MultiSelect", () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items: itemsWithDisabled,
|
props: {
|
||||||
|
items: itemsWithDisabled,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
|
@ -354,33 +377,51 @@ describe("MultiSelect", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("custom formatting", () => {
|
describe("accessibility", () => {
|
||||||
it("should handle custom itemToString", () => {
|
it("handles hidden label", () => {
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
selectedIds: ["0"],
|
items,
|
||||||
itemToString: (item) => `${item.text} (${item.id})`,
|
titleText: "Contact methods",
|
||||||
|
hideLabel: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = screen.getByText("Contact methods");
|
||||||
|
expect(label).toHaveClass("bx--visually-hidden");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("custom formatting", () => {
|
||||||
|
it("handles custom itemToString", () => {
|
||||||
|
render(MultiSelect, {
|
||||||
|
props: {
|
||||||
|
items,
|
||||||
|
selectedIds: ["0"],
|
||||||
|
itemToString: (item) => `${item.text} (${item.id})`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("Slack (0)")).toBeInTheDocument();
|
expect(screen.getByText("Slack (0)")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle custom itemToInput", async () => {
|
it("handles custom itemToInput", async () => {
|
||||||
render(MultiSelect, {
|
render(MultiSelect, {
|
||||||
items,
|
props: {
|
||||||
itemToInput: (item) => ({
|
items,
|
||||||
name: `contact_${item.id}`,
|
itemToInput: (item) => ({
|
||||||
value: item.text.toLowerCase(),
|
name: `contact_${item.id}`,
|
||||||
}),
|
value: item.text.toLowerCase(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await openMenu();
|
await openMenu();
|
||||||
const checkbox = screen.getByText("Slack");
|
const checkbox = screen.getByText("Slack");
|
||||||
const checkboxWrapper = checkbox.closest(".bx--checkbox-wrapper");
|
const checkboxWrapper = checkbox.closest(".bx--checkbox-wrapper");
|
||||||
assert(checkboxWrapper);
|
const checkboxInput = checkboxWrapper?.querySelector("input");
|
||||||
|
|
||||||
const checkboxInput = checkboxWrapper.querySelector("input");
|
|
||||||
expect(checkboxInput).toHaveAttribute("name", "contact_0");
|
expect(checkboxInput).toHaveAttribute("name", "contact_0");
|
||||||
|
expect(checkboxInput).toHaveAttribute("value", "slack");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
12
tests/MultiSelect/MultiSelectSlot.test.svelte
Normal file
12
tests/MultiSelect/MultiSelectSlot.test.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { MultiSelect } from "carbon-components-svelte";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
export let items: ComponentProps<MultiSelect>["items"] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<MultiSelect {items} let:item let:index>
|
||||||
|
<div>
|
||||||
|
<strong>{item.id} {item.text} {index}</strong>
|
||||||
|
</div>
|
||||||
|
</MultiSelect>
|
Loading…
Add table
Add a link
Reference in a new issue