Add tests for SuggestBox component
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { FormEvent, ReactNode } from "react";
|
||||
import { useCallback, useMemo, useRef, useState, useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
@@ -50,7 +50,7 @@ const NoSuggestionsText = styled.div`
|
||||
padding-left: 22px;
|
||||
`;
|
||||
|
||||
type Props<T extends Option<T>> = {
|
||||
export type SuggestBoxProps<T extends Option<T>> = {
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
options: T[];
|
||||
@@ -76,6 +76,14 @@ type Props<T extends Option<T>> = {
|
||||
disabled?: boolean;
|
||||
|
||||
"aria-label"?: string;
|
||||
|
||||
/**
|
||||
* Can be used to render a different component for the input. This is used
|
||||
* in testing to use default HTML components rather than the VSCodeTextField
|
||||
* for easier testing.
|
||||
* @param props The props returned by `getReferenceProps` of {@link useInteractions}
|
||||
*/
|
||||
renderInputComponent?: (props: Record<string, unknown>) => ReactNode;
|
||||
};
|
||||
|
||||
export const SuggestBox = <T extends Option<T>>({
|
||||
@@ -87,7 +95,8 @@ export const SuggestBox = <T extends Option<T>>({
|
||||
getDetails,
|
||||
disabled,
|
||||
"aria-label": ariaLabel,
|
||||
}: Props<T>) => {
|
||||
renderInputComponent = (props) => <Input {...props} />,
|
||||
}: SuggestBoxProps<T>) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
|
||||
@@ -150,8 +159,8 @@ export const SuggestBox = <T extends Option<T>>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
{...getReferenceProps({
|
||||
{renderInputComponent(
|
||||
getReferenceProps({
|
||||
ref: refs.setReference,
|
||||
value,
|
||||
onInput: handleInput,
|
||||
@@ -170,8 +179,8 @@ export const SuggestBox = <T extends Option<T>>({
|
||||
}
|
||||
},
|
||||
disabled,
|
||||
})}
|
||||
/>
|
||||
}),
|
||||
)}
|
||||
{isOpen && (
|
||||
<FloatingPortal>
|
||||
{value && suggestionItems.length === 0 && (
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { render as reactRender, screen } from "@testing-library/react";
|
||||
import type { SuggestBoxProps } from "../SuggestBox";
|
||||
import { SuggestBox } from "../SuggestBox";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
|
||||
type TestOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
followup?: TestOption[];
|
||||
};
|
||||
|
||||
const options: TestOption[] = [
|
||||
{
|
||||
label: "Argument[self]",
|
||||
value: "Argument[self]",
|
||||
},
|
||||
{
|
||||
label: "Argument[0]",
|
||||
value: "Argument[0]",
|
||||
followup: [
|
||||
{
|
||||
label: "Element[0]",
|
||||
value: "Argument[0].Element[0]",
|
||||
},
|
||||
{
|
||||
label: "Element[1]",
|
||||
value: "Argument[0].Element[1]",
|
||||
},
|
||||
{
|
||||
label: "Element[any]",
|
||||
value: "Argument[0].Element[any]",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Argument[1]",
|
||||
value: "Argument[1]",
|
||||
},
|
||||
{
|
||||
label: "Argument[text_rep:]",
|
||||
value: "Argument[text_rep:]",
|
||||
},
|
||||
{
|
||||
label: "Argument[block]",
|
||||
value: "Argument[block]",
|
||||
followup: [
|
||||
{
|
||||
label: "Parameter[0]",
|
||||
value: "Argument[block].Parameter[0]",
|
||||
followup: [
|
||||
{
|
||||
label: "Element[:query]",
|
||||
value: "Argument[block].Parameter[0].Element[:query]",
|
||||
},
|
||||
{
|
||||
label: "Element[:parameters]",
|
||||
value: "Argument[block].Parameter[0].Element[:parameters]",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Parameter[1]",
|
||||
value: "Argument[block].Parameter[1]",
|
||||
followup: [
|
||||
{
|
||||
label: "Field[@query]",
|
||||
value: "Argument[block].Parameter[1].Field[@query]",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "ReturnValue",
|
||||
value: "ReturnValue",
|
||||
},
|
||||
];
|
||||
|
||||
describe("SuggestBox", () => {
|
||||
const onChange = jest.fn();
|
||||
const parseValueToTokens = jest.fn();
|
||||
const render = (props?: Partial<SuggestBoxProps<TestOption>>) =>
|
||||
reactRender(
|
||||
<SuggestBox
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
parseValueToTokens={parseValueToTokens}
|
||||
renderInputComponent={(props) => <input {...props} />}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
onChange.mockReset();
|
||||
parseValueToTokens
|
||||
.mockReset()
|
||||
.mockImplementation((value: string) => value.split("."));
|
||||
});
|
||||
|
||||
it("does not render the options by default", () => {
|
||||
render();
|
||||
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the options after clicking on the text field", async () => {
|
||||
render();
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
|
||||
expect(screen.getAllByRole("option")).toHaveLength(options.length);
|
||||
});
|
||||
|
||||
it("calls onChange after entering text", async () => {
|
||||
render({
|
||||
value: "Argument[block]",
|
||||
});
|
||||
|
||||
await userEvent.type(screen.getByRole("combobox"), ".");
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("Argument[block].");
|
||||
});
|
||||
|
||||
it("calls onChange after clearing text", async () => {
|
||||
render({
|
||||
value: "Argument[block].",
|
||||
});
|
||||
|
||||
await userEvent.clear(screen.getByRole("combobox"));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("renders matching options with a single token", async () => {
|
||||
render({
|
||||
value: "block",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
|
||||
expect(screen.getByRole("option")).toHaveTextContent("Argument[block]");
|
||||
});
|
||||
|
||||
it("renders followup options with a token and an empty token", async () => {
|
||||
render({
|
||||
value: "Argument[block].",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
|
||||
expect(screen.getAllByRole("option")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders matching followup options with two tokens", async () => {
|
||||
render({
|
||||
value: "Argument[block].1",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
|
||||
expect(screen.getByRole("option")).toHaveTextContent("Parameter[1]");
|
||||
});
|
||||
|
||||
it("closes the options when selecting an option", async () => {
|
||||
render({
|
||||
value: "Argument[block].1",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows no suggestions with no matching followup options", async () => {
|
||||
render({
|
||||
value: "Argument[block].block",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("No suggestions.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can navigate the options using the keyboard", async () => {
|
||||
render({
|
||||
value: "",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
await userEvent.keyboard(
|
||||
"{ArrowDown}{ArrowDown}{ArrowUp}{ArrowDown}{ArrowDown}{Enter}",
|
||||
);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("Argument[text_rep:]");
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can use loop navigation when using the keyboard", async () => {
|
||||
render({
|
||||
value: "",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
await userEvent.keyboard("{ArrowUp}{ArrowUp}{Enter}");
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith("Argument[block]");
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("can close the options using escape", async () => {
|
||||
render({
|
||||
value: "",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
|
||||
expect(screen.getAllByRole("option")).toHaveLength(options.length);
|
||||
|
||||
await userEvent.keyboard("{Escape}");
|
||||
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the options when using backspace on a selected option", async () => {
|
||||
render({
|
||||
value: "Argument[block].1",
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox"));
|
||||
await userEvent.keyboard("{Enter}");
|
||||
|
||||
expect(screen.queryByRole("option")).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.keyboard("{Backspace}");
|
||||
|
||||
expect(screen.getAllByRole("option")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user