import userEvent from "@testing-library/user-event"; import { render, screen, waitFor } from "@testing-library/react"; import React, { createRef, FormEvent, useState } from "react"; import { expect } from "vitest"; import { within } from "@testing-library/dom"; import { CunninghamProvider } from ":/components/Provider"; import { Select, SelectHandle, SelectProps, } from ":/components/Forms/Select/index"; import { expectMenuToBeClosed, expectMenuToBeOpen, expectOptions, expectOptionToBeDisabled, expectOptionToBeUnselected, expectSelectedOptions, expectSelectedOptionsText, } from ":/components/Forms/Select/test-utils"; import { Button } from ":/components/Button"; describe("", () => { describe("Simple", async () => { it("should select options and clear them one by one", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); const label = screen.getByText("Cities")!.parentElement!; // Expect no options to be selected. expectSelectedOptions([]); // Make sure the label is set as placeholder. expect(Array.from(label.classList)).toContain("placeholder"); await user.click(input); // Make sure the menu is opened and options are rendered. expectMenuToBeOpen(menu); expect( screen.queryByRole("option", { name: "Paris" }), ).toBeInTheDocument(); // Make sure the option is not selected. const option: HTMLLIElement = screen.getByRole("option", { name: "London", }); expectOptionToBeUnselected(option); // Select an option. await user.click(option); // Make sure the option is selected. expectSelectedOptions(["London"]); // Make sure the menu stays open. expectMenuToBeOpen(menu); // Make sure the option is removed from the menu. expect( screen.queryByRole("option", { name: "London" }), ).not.toBeInTheDocument(); // Select other options. await user.click( screen.getByRole("option", { name: "Tokyo", }), ); await user.click( screen.getByRole("option", { name: "Panama", }), ); await waitFor(() => expectSelectedOptions(["London", "Tokyo", "Panama"])); // Clear one option. await user.click( within( screen.getByText("Panama").parentNode as HTMLDivElement, ).getByRole("button", { name: "Clear selection", }), ); await waitFor(() => expectSelectedOptions(["London", "Tokyo"])); // Clear one option. await user.click( within( screen.getByText("London").parentNode as HTMLDivElement, ).getByRole("button", { name: "Clear selection", }), ); await waitFor(() => expectSelectedOptions(["Tokyo"])); // Clear one option. await user.click( within( screen.getByText("Tokyo").parentNode as HTMLDivElement, ).getByRole("button", { name: "Clear selection", }), ); expectSelectedOptions([]); }); it("should clear all selected options", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); await user.click(input); // Select options. await user.click( screen.getByRole("option", { name: "London", }), ); await user.click( screen.getByRole("option", { name: "Tokyo", }), ); await user.click( screen.getByRole("option", { name: "Panama", }), ); expectSelectedOptions(["London", "Tokyo", "Panama"]); // Clear selection. const clearButton = screen.getByRole("button", { name: "Clear all selections", }); await user.click(clearButton); expectSelectedOptions([]); }); it("should select with defaultValue using label", async () => { render( , ); expectSelectedOptions(["New York", "Tokyo"]); }); it("should select with defaultValue using value", async () => { render( , ); expectSelectedOptions(["New York", "Tokyo"]); }); it("works controlled", async () => { const Wrapper = () => { const [value, setValue] = useState(["london"]); return ( Value = {JSON.stringify(value)}| setValue([])}>Clear setValue(e.target.value as string[])} /> ); }; render(); const input = screen.getByRole("combobox", { name: "Cities", }); // Make sure value is selected. screen.getByText('Value = ["london"]|'); // Change value. const user = userEvent.setup(); await user.click(input); // Make sure the option is removed from the menu. expect( screen.queryByRole("option", { name: "London" }), ).not.toBeInTheDocument(); // Select an option. await user.click( screen.getByRole("option", { name: "New York", }), ); // Make sure value is selected. screen.getByText('Value = ["london","new_york"]|'); // clear value. const button = screen.getByRole("button", { name: "Clear", }); await user.click(button); // Make sure value is cleared. screen.getByText("Value = []|"); }); it("renders disabled", async () => { render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); expect(input).toHaveAttribute("disabled"); const button: HTMLButtonElement = document.querySelector( ".c__select__inner__actions__open", )!; expect(button).toBeDisabled(); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); expectMenuToBeClosed(menu); const user = userEvent.setup(); // Try to open the menu. await user.click(input); // Make sure menu is still closed. expectMenuToBeClosed(menu); // Make sure no value is rendered expectSelectedOptions([]); }); it("renders with text", async () => { render( , ); screen.getByText("This is a text"); }); it("renders with state=error", async () => { render( , ); expect( document.querySelector(".c__select.c__select--error"), ).toBeInTheDocument(); }); it("renders with state=success", async () => { render( , ); expect( document.querySelector(".c__select.c__select--success"), ).toBeInTheDocument(); }); it("submits form data", async () => { let formData: any; const Wrapper = () => { const onSubmit = (e: FormEvent) => { e.preventDefault(); const data = new FormData(e.currentTarget); formData = { cities: data.getAll("cities"), }; }; return ( Submit ); }; render(); const user = userEvent.setup(); const input = screen.getByRole("combobox", { name: "Cities", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); const button = screen.getByRole("button", { name: "Submit", }); // Submit the form being empty. expectSelectedOptions([]); await user.click(button); expect(formData).toEqual({ cities: [], }); // Select an option await user.click(input); expectMenuToBeOpen(menu); await user.click( screen.getByRole("option", { name: "New York", }), ); expectSelectedOptions(["New York"]); // Close the menu. await user.click(input); expectMenuToBeClosed(menu); // Submit the form being fulfilled. await user.click(button); expect(formData).toEqual({ cities: ["new_york"], }); expectSelectedOptions(["New York"]); // Select another option. await user.click(input); await user.click( await screen.findByRole("option", { name: "Paris", }), ); expectSelectedOptions(["New York", "Paris"]); // Close the menu. await user.click(input); expectMenuToBeClosed(menu); // Submit the form being fulfilled. await user.click(button); expect(formData).toEqual({ cities: ["new_york", "paris"], }); // Clear selection. const clearButton = screen.getByRole("button", { name: "Clear all selections", }); await userEvent.click(clearButton); // Submit again. await user.click(button); expect(formData).toEqual({ cities: [], }); }); it("should not be clearable", async () => { render( , ); screen.getByRole("combobox", { name: "Cities", }); // Make sure default value is rendered. expectSelectedOptions(["Paris"]); // Make sure the clear button is not rendered. expect( screen.queryByRole("button", { name: "Clear all selections", }), ).not.toBeInTheDocument(); }); it("is not possible to select disabled options", async () => { render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); // Make sure the select is empty. expectSelectedOptions([]); const user = userEvent.setup(); await user.click(input); expectMenuToBeOpen(menu); // Make sure the disabled option is not selectable. let option: HTMLLIElement = screen.getByRole("option", { name: "New York", }); expectOptionToBeDisabled(option); // Try to click on the disabled option. await user.click(option); expectMenuToBeOpen(menu); // Make sure the select is still empty. expectSelectedOptions([]); // Select a not disabled option. option = screen.getByRole("option", { name: "Tokyo", }); await user.click(option); await waitFor(() => expectSelectedOptions(["Tokyo"])); }); it("is accessible if the the label is not shown", async () => { render( , ); // Make sure the input is accessible. screen.getByRole("combobox", { name: "Cities", }); const label = screen.getByText("Cities")!.parentElement!; expect(Array.from(label.classList)).toContain("c__offscreen"); }); it("is possible to select again the last deleted item", async () => { render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); expectSelectedOptions([]); const user = userEvent.setup(); await user.click(input); // Select options. await user.click( screen.getByRole("option", { name: "Paris", }), ); expectSelectedOptions(["Paris"]); // Clear selection. const clearButton = screen.getByRole("button", { name: "Clear all selections", }); await user.click(clearButton); expectSelectedOptions([]); // Select again London. await user.click(input); await user.click( screen.getByRole("option", { name: "Paris", }), ); expectSelectedOptions(["Paris"]); }); it("blurs from ref", async () => { const ref = createRef(); render( , ); const input = screen.getByRole("combobox", { name: "City", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "City", }); const user = userEvent.setup(); // Make sure the select is not focused. expect(document.activeElement?.className).toEqual(""); // Focus the select. await user.click(input); expectMenuToBeOpen(menu); expect(document.activeElement?.className).toContain("c__button"); // Blur the select. ref.current?.blur(); // Make sure the select is blured. await waitFor(() => expectMenuToBeClosed(menu)); expect(document.activeElement?.className).toEqual(""); }); it("renders custom options", async () => { const Wrapper = (props: SelectProps) => { return ( ); }; const props: SelectProps = { label: "City", multi: true, options: [ { label: "Paris", value: "paris", render: () => ( Paris ), }, { label: "Panama", value: "panama", render: () => ( Panama ), }, { label: "London", value: "london", render: () => ( London ), }, ], }; const { rerender } = render(); const input = screen.getByRole("combobox", { name: "City", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "City", }); const valueRendered = document.querySelector( ".c__select__inner__value", ) as HTMLElement; const user = userEvent.setup(); expectSelectedOptions([]); await user.click(input); expectMenuToBeOpen(menu); screen.getByRole("img", { name: "Paris flag" }); screen.getByRole("img", { name: "Panama flag" }); screen.getByRole("img", { name: "London flag" }); // Select Paris await user.click( screen.getByRole("option", { name: "Paris flag Paris" }), ); await user.click( screen.getByRole("option", { name: "London flag London" }), ); // Make sure only the label is rendered by default. expectSelectedOptions(["Paris", "London"]); expect( within(valueRendered).queryByRole("img", { name: "Paris flag", }), ).not.toBeInTheDocument(); expect( within(valueRendered).queryByRole("img", { name: "London flag", }), ).not.toBeInTheDocument(); // Now showLabelWhenSelected to false. rerender(); // Make sure the HTML content of the options is rendered. expectSelectedOptions(["Paris", "London"]); within(valueRendered).getByRole("img", { name: "Paris flag", }); within(valueRendered).getByRole("img", { name: "London flag", }); }); it("renders checkboxes in the menu and selected options as text when using monoline mode", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); const label = screen.getByText("Cities")!.parentElement!; const expectCheckedOptions = ({ checked, notChecked, }: { checked: string[]; notChecked: string[]; }) => { checked.forEach((option) => { const optionElement = screen.getByRole("option", { name: option }); const checkbox: HTMLInputElement = within(optionElement).getByRole("checkbox"); expect(checkbox.checked).toEqual(true); }); notChecked.forEach((option) => { const optionElement = screen.getByRole("option", { name: option }); const checkbox: HTMLInputElement = within(optionElement).getByRole("checkbox"); expect(checkbox.checked).toEqual(false); }); }; // Expect no options to be selected. expectSelectedOptionsText([]); // Make sure the label is set as placeholder. expect(Array.from(label.classList)).toContain("placeholder"); await user.click(input); // Make sure the menu is opened and options are rendered. expectMenuToBeOpen(menu); expectOptions(["Paris", "Panama", "London", "New York", "Tokyo"]); expectCheckedOptions({ checked: [], notChecked: ["Paris", "Panama", "London", "New York", "Tokyo"], }); // Make sure the option is not selected. const option: HTMLLIElement = screen.getByRole("option", { name: "London", }); expectOptionToBeUnselected(option); // Select an option. await user.click(option); // Make sure the option is selected. expectSelectedOptionsText(["London"]); // Make sure the menu stays open. expectMenuToBeOpen(menu); // Make sure the option is still present in the menu and is highlighted. expectCheckedOptions({ checked: ["London"], notChecked: ["Paris", "Panama", "New York", "Tokyo"], }); // Select Paris await user.click(screen.getByRole("option", { name: "Paris" })); // Make sure the option is selected. expectSelectedOptionsText(["London", "Paris"]); screen.logTestingPlaygroundURL(); // Make sure the menu stays open. expectMenuToBeOpen(menu); // Make sure the option is still present in the menu and is highlighted. expectCheckedOptions({ checked: ["London", "Paris"], notChecked: ["Panama", "New York", "Tokyo"], }); // Click on London and make sure it is removed from the selected. await user.click(screen.getByRole("option", { name: "London" })); // We need to move the cursor away from London otherwise the option will be highlighted. await user.hover(screen.getByRole("option", { name: "Paris" })); // Make sure the option is not selected anymore. expectSelectedOptionsText(["Paris"]); // Make sure the menu stays open. expectMenuToBeOpen(menu); // Make sure the option is still present in the menu and is not highlighted anymore. await waitFor(() => expectCheckedOptions({ checked: ["Paris"], notChecked: ["London", "Panama", "New York", "Tokyo"], }), ); }); }); describe("Searchable", async () => { it("shows all options when clicking on the input", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); // It returns the input. expect(input.tagName).toEqual("INPUT"); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); expectMenuToBeClosed(menu); // Click on the input. await user.click(input); expectMenuToBeOpen(menu); expectOptions(["Paris", "Panama", "London", "New York", "Tokyo"]); // Select an option. const option: HTMLLIElement = screen.getByRole("option", { name: "New York", }); await user.click(option); expectMenuToBeOpen(menu); expectSelectedOptions(["New York"]); }); it("filters options when typing", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); // It returns the input. expect(input.tagName).toEqual("INPUT"); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); expectMenuToBeClosed(menu); // Click on the input. await user.click(input); expectMenuToBeOpen(menu); expectOptions(["Paris", "Panama", "London", "New York", "Tokyo"]); // Verify that filtering works. await user.type(input, "Pa"); expectMenuToBeOpen(menu); expectOptions(["Paris", "Panama"]); await user.type(input, "r"); expectOptions(["Paris"]); }); it("selects the option on enter", async () => { const user = userEvent.setup(); render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); // It returns the input. expect(input.tagName).toEqual("INPUT"); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); expectMenuToBeClosed(menu); // Click on the input. await user.click(input); expectMenuToBeOpen(menu); expectOptions(["Paris", "Panama", "London", "New York", "Tokyo"]); // Verify that filtering works. await user.type(input, "Par"); expectOptions(["Paris"]); await user.type(input, "{enter}", { skipClick: true }); await waitFor(() => expectSelectedOptions(["Paris"])); }); it("remove last selected value on backspace", async () => { const user = userEvent.setup(); render( , ); expectSelectedOptions(["Panama", "New York", "Tokyo"]); const input = screen.getByRole("combobox", { name: "Cities", }); // Type few characters. await user.type(input, "Par"); expectOptions(["Paris"]); // Remove "Par" characters. await user.type(input, "{backspace}"); await user.type(input, "{backspace}"); await user.type(input, "{backspace}"); expectSelectedOptions(["Panama", "New York", "Tokyo"]); await user.type(input, "{backspace}"); expectSelectedOptions(["Panama", "New York"]); await user.type(input, "{backspace}"); expectSelectedOptions(["Panama"]); await user.type(input, "{backspace}"); expectSelectedOptions([]); // One last to be sure it works when already empty. await user.type(input, "{backspace}"); expectSelectedOptions([]); }); it("clears value", async () => { const user = userEvent.setup(); render( , ); expectSelectedOptions(["Panama", "London", "Tokyo"]); // Clear selection. const clearButton = screen.getByRole("button", { name: "Clear all selections", }); await user.click(clearButton); expectSelectedOptions([]); }); it("should select with defaultValue using label", async () => { render( , ); expectSelectedOptions(["Panama", "London", "Tokyo"]); }); it("should select with defaultValue using value", async () => { render( , ); expectSelectedOptions(["New York", "Tokyo"]); }); it("should not select any value if there is not match", async () => { render( , ); expectSelectedOptions(["New York"]); }); it("works controlled", async () => { const Wrapper = () => { const [value, setValue] = useState(["london"]); return ( Value = {JSON.stringify(value)}| setValue([])}>Clear setValue(e.target.value as string[])} /> ); }; render(); const input = screen.getByRole("combobox", { name: "Cities", }); // Make sure value is selected. screen.getByText('Value = ["london"]|'); expectSelectedOptions(["London"]); // Select one more value. const user = userEvent.setup(); await user.click(input); await waitFor(() => expectOptions(["Paris", "New York", "Tokyo"])); await user.type(input, "New", { skipClick: true }); await waitFor(() => expectOptions(["New York"])); await user.type(input, "{enter}}", { skipClick: true }); expectSelectedOptions(["London", "New York"]); screen.getByText('Value = ["london","new_york"]|'); // clear value. const button = screen.getByRole("button", { name: "Clear", }); await user.click(button); // Make sure value is cleared. screen.getByText("Value = []|"); }); it("renders disabled", async () => { render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); expect(input).toHaveAttribute("disabled"); const button: HTMLButtonElement = document.querySelector( ".c__select__inner__actions__open", )!; expect(button).toBeDisabled(); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "Cities", }); expectMenuToBeClosed(menu); const user = userEvent.setup(); // Try to open the menu. await user.click(input); // Make sure menu is still closed. expectMenuToBeClosed(menu); // Make sure no value is rendered const valueRendered = document.querySelector(".c__select__inner__value"); expect(valueRendered).toHaveTextContent(""); // Try to type await user.type(input, "Pa"); expectMenuToBeClosed(menu); }); it("renders menu empty placeholder when there is no more options to display", async () => { render( , ); const input = screen.getByRole("combobox", { name: "Cities", }); // Expect no options to be selected. expectSelectedOptions([]); const user = userEvent.setup(); await user.click(input); await user.click( screen.getByRole("option", { name: "Paris", }), ); expectSelectedOptions(["Paris"]); expect( screen.queryByText("No options available"), ).not.toBeInTheDocument(); await user.click( screen.getByRole("option", { name: "London", }), ); expectSelectedOptions(["Paris", "London"]); screen.getByText("No options available"); }); it("blurs from ref", async () => { const ref = createRef(); render( , ); const input = screen.getByRole("combobox", { name: "City", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "City", }); const user = userEvent.setup(); // Make sure the select is not focused. expect(document.activeElement?.tagName).toEqual("BODY"); // Focus the select by focusing input. await user.click(input); expectMenuToBeOpen(menu); expect(document.activeElement?.tagName).toEqual("INPUT"); // Blur the select. ref.current?.blur(); // Make sure the select is blured. await waitFor(() => expectMenuToBeClosed(menu)); expect(document.activeElement?.tagName).toEqual("BODY"); }); it("renders custom options", async () => { const Wrapper = (props: SelectProps) => { return ( ); }; const props: SelectProps = { label: "City", multi: true, searchable: true, options: [ { label: "Paris", value: "paris", render: () => ( Paris ), }, { label: "Panama", value: "panama", render: () => ( Panama ), }, { label: "London", value: "london", render: () => ( London ), }, ], }; const { rerender } = render(); const input = screen.getByRole("combobox", { name: "City", }); const menu: HTMLDivElement = screen.getByRole("listbox", { name: "City", }); const valueRendered = document.querySelector( ".c__select__inner__value", ) as HTMLElement; const user = userEvent.setup(); expectSelectedOptions([]); await user.click(input); expectMenuToBeOpen(menu); screen.getByRole("img", { name: "Paris flag" }); screen.getByRole("img", { name: "Panama flag" }); screen.getByRole("img", { name: "London flag" }); // Filter options. await user.type(input, "Pa"); screen.getByRole("img", { name: "Paris flag" }); screen.getByRole("img", { name: "Panama flag" }); expect( screen.queryByRole("img", { name: "London flag" }), ).not.toBeInTheDocument(); // Select Paris await user.click( screen.getByRole("option", { name: "Paris flag Paris" }), ); // Filter to find London. await user.clear(input); expect( screen.queryByRole("img", { name: "Paris flag" }), ).not.toBeInTheDocument(); screen.getByRole("img", { name: "Panama flag" }); screen.getByRole("img", { name: "London flag" }); await user.type(input, "Lo"); expect( screen.queryByRole("img", { name: "Paris flag" }), ).not.toBeInTheDocument(); expect( screen.queryByRole("img", { name: "Panama flag" }), ).not.toBeInTheDocument(); screen.getByRole("img", { name: "London flag" }); // Select London. await user.click( screen.getByRole("option", { name: "London flag London" }), ); // Make sure only the label is rendered by default. expectSelectedOptions(["Paris", "London"]); expect( within(valueRendered).queryByRole("img", { name: "Paris flag", }), ).not.toBeInTheDocument(); expect( within(valueRendered).queryByRole("img", { name: "London flag", }), ).not.toBeInTheDocument(); // Now showLabelWhenSelected to false. rerender(); // Make sure the HTML content of the options is rendered. expectSelectedOptions(["Paris", "London"]); within(valueRendered).getByRole("img", { name: "Paris flag", }); within(valueRendered).getByRole("img", { name: "London flag", }); }); }); });