diff --git a/README.md b/README.md
index e16e108..497da9a 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,7 @@ const { getByRole, getByLabelText } = render(
expect(getByRole("form")).toHaveFormValues({ food: "" });
// start typing to trigger the `loadOptions`
-fireEvent.change(getByLabelText("Food"), { target: { value: "Choc" } });
+userEvent.type(getByLabelText("Food"), "Choc");
await selectEvent.select(getByLabelText("Food"), "Chocolate");
expect(getByRole("form")).toHaveFormValues({
food: ["chocolate"],
diff --git a/package.json b/package.json
index f6ff54a..1ed4662 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,8 @@
},
"homepage": "https://github.com/romgain/react-select-event#readme",
"dependencies": {
- "@testing-library/dom": ">=7"
+ "@testing-library/dom": ">=7",
+ "@testing-library/user-event": ">=14"
},
"devDependencies": {
"@babel/core": "^7.4.5",
@@ -46,14 +47,14 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@testing-library/jest-dom": "^5.0.1",
- "@testing-library/react": "^12.1.5",
+ "@testing-library/react": "^14.0.0",
"@types/jest": "^29.1.2",
- "@types/react": "^17.0.47",
+ "@types/react": "^18.2.6",
"@types/react-select": "^5.0.1",
"jest": "^27.0.4",
"prettier": "^2.0.2",
- "react": "^17.0.2",
- "react-dom": "^17.0.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
"react-select": "^5.0.0",
"rimraf": "^3.0.0",
"rollup": "^2.0.3",
diff --git a/src/__tests__/select-event.test.tsx b/src/__tests__/select-event.test.tsx
index bf9d78c..cc75fa3 100644
--- a/src/__tests__/select-event.test.tsx
+++ b/src/__tests__/select-event.test.tsx
@@ -1,6 +1,7 @@
import "@testing-library/jest-dom/extend-expect";
-import { fireEvent, render } from "@testing-library/react";
+import { render } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import React from "react";
import Select from "react-select";
@@ -56,20 +57,20 @@ const renderForm = (select: React.ReactNode) => {
};
describe("The openMenu event helper", () => {
- it("opens the menu", () => {
+ it("opens the menu", async () => {
const { getByLabelText, queryByText } = renderForm(
);
// option is not yet visible
expect(queryByText("Chocolate")).toBeNull();
- selectEvent.openMenu(getByLabelText("Food"));
+ await selectEvent.openMenu(getByLabelText("Food"));
// option can now be seen because menu is open
expect(queryByText("Chocolate")).toBeInTheDocument();
});
it("does not prevent selecting options", async () => {
const { form, input, getByText } = renderForm();
- selectEvent.openMenu(input);
+ await selectEvent.openMenu(input);
expect(getByText("Chocolate")).toBeInTheDocument();
expect(getByText("Vanilla")).toBeInTheDocument();
expect(getByText("Strawberry")).toBeInTheDocument();
@@ -77,6 +78,34 @@ describe("The openMenu event helper", () => {
await selectEvent.select(input, "Strawberry");
expect(form).toHaveFormValues({ food: "strawberry" });
});
+
+ it("allows passing custom userEvent option", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+ jest.spyOn(user, "type");
+
+ const { input } = renderForm();
+
+ await selectEvent.openMenu(input, { user });
+
+ expect(user.click).toHaveBeenCalledWith(input);
+ expect(user.type).toHaveBeenCalledWith(input, "{ArrowDown}");
+ });
+
+ it("allows passing custom userEvent option with setup", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+ jest.spyOn(user, "type");
+
+ const userSelectEvent = selectEvent.setup(user);
+
+ const { input } = renderForm();
+
+ await userSelectEvent.openMenu(input);
+
+ expect(user.click).toHaveBeenCalledWith(input);
+ expect(user.type).toHaveBeenCalledWith(input, "{ArrowDown}");
+ });
});
describe("The select event helpers", () => {
@@ -240,6 +269,7 @@ describe("The select event helpers", () => {
});
it("selects an option in an async input", async () => {
+ const user = userEvent.setup();
const loadOptions = (_: string, callback: Callback) =>
setTimeout(() => callback(OPTIONS), 100);
const { form, input } = renderForm(
@@ -248,7 +278,7 @@ describe("The select event helpers", () => {
expect(form).toHaveFormValues({ food: "" });
// start typing to trigger the `loadOptions`
- fireEvent.change(input, { target: { value: "Choc" } });
+ await user.type(input, "Choc");
await selectEvent.select(input, "Chocolate");
expect(form).toHaveFormValues({ food: "chocolate" });
});
@@ -344,6 +374,42 @@ describe("The select event helpers", () => {
expect(form).toHaveFormValues({ food: "vanilla" });
});
+ it("allows passing custom userEvent option", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+ jest.spyOn(user, "type");
+
+ const { input } = renderForm();
+
+ await selectEvent.select(input, "Chocolate", { user });
+
+ // Open the dropdown
+ expect(user.type).toHaveBeenCalledWith(input, "{ArrowDown}");
+ expect(user.click).toHaveBeenNthCalledWith(1, input);
+
+ // Difficult to get correct element here but it's fine as long as we get an element
+ expect(user.click).toHaveBeenNthCalledWith(2, expect.any(HTMLDivElement));
+ });
+
+ it("allows passing custom userEvent option with setup", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+ jest.spyOn(user, "type");
+
+ const userSelectEvent = selectEvent.setup(user);
+
+ const { input } = renderForm();
+
+ await userSelectEvent.select(input, "Chocolate");
+
+ // Open the dropdown
+ expect(user.type).toHaveBeenCalledWith(input, "{ArrowDown}");
+ expect(user.click).toHaveBeenNthCalledWith(1, input);
+
+ // Difficult to get correct element here but it's fine as long as we get an element
+ expect(user.click).toHaveBeenNthCalledWith(2, expect.any(HTMLDivElement));
+ });
+
describe("when asynchronously generating the list of options", () => {
// from https://github.com/JedWatson/react-select/blob/v3.0.0/docs/examples/CreatableAdvanced.js
// mixed with Async Creatable Example from https://react-select.com/creatable
@@ -416,6 +482,56 @@ describe("The select event helpers", () => {
expect(form).toHaveFormValues({ food: "papaya" });
});
+
+ it("allows passing custom userEvent option", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+ jest.spyOn(user, "type");
+
+ const { input } = renderForm();
+
+ await selectEvent.create(input, "papaya", { user });
+
+ // Open the dropdown
+ expect(user.click).toHaveBeenNthCalledWith(1, input);
+ expect(user.type).toHaveBeenCalledWith(input, "{ArrowDown}");
+
+ // Create option
+ expect(user.type).toHaveBeenNthCalledWith(2, input, "papaya");
+
+ // Open dropdown again
+ expect(user.click).toHaveBeenNthCalledWith(2, input);
+
+ // Select the new option
+ // Difficult to get correct element here but it's fine as long as we get an element
+ expect(user.click).toHaveBeenNthCalledWith(3, expect.any(HTMLDivElement));
+ });
+
+ it("allows passing custom userEvent option with setup", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+ jest.spyOn(user, "type");
+
+ const userSelectEvent = selectEvent.setup(user);
+
+ const { input } = renderForm();
+
+ await userSelectEvent.create(input, "papaya");
+
+ // Open the dropdown
+ expect(user.click).toHaveBeenNthCalledWith(1, input);
+ expect(user.type).toHaveBeenCalledWith(input, "{ArrowDown}");
+
+ // Create option
+ expect(user.type).toHaveBeenNthCalledWith(2, input, "papaya");
+
+ // Open dropdown again
+ expect(user.click).toHaveBeenNthCalledWith(2, input);
+
+ // Select the new option
+ // Difficult to get correct element here but it's fine as long as we get an element
+ expect(user.click).toHaveBeenNthCalledWith(3, expect.any(HTMLDivElement));
+ });
});
describe("when rendering the dropdown in a portal", () => {
@@ -452,39 +568,119 @@ describe("The select event helpers", () => {
await selectEvent.create(input, "papaya", { container: document.body });
expect(form).toHaveFormValues({ food: "papaya" });
});
+ });
+});
- it("clears the first item in a multi-select dropdown", async () => {
- const { form, input } = renderForm(
-
- );
- expect(form).toHaveFormValues({
- food: ["chocolate", "vanilla", "strawberry"],
- });
-
- await selectEvent.clearFirst(input);
- expect(form).toHaveFormValues({ food: ["vanilla", "strawberry"] });
+describe("clearFirst", () => {
+ it("clears the first item in a multi-select dropdown", async () => {
+ const { form, input } = renderForm(
+
+ );
+ expect(form).toHaveFormValues({
+ food: ["chocolate", "vanilla", "strawberry"],
});
- it("clears all items", async () => {
- const { form, input } = renderForm(
-
- );
- expect(form).toHaveFormValues({
- food: ["chocolate", "vanilla", "strawberry"],
- });
+ await selectEvent.clearFirst(input);
+ expect(form).toHaveFormValues({ food: ["vanilla", "strawberry"] });
+ });
- await selectEvent.clearAll(input);
- expect(form).toHaveFormValues({ food: "" });
+ it("allows passing custom userEvent option", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+
+ const { input } = renderForm(
+
+ );
+
+ await selectEvent.clearFirst(input, { user });
+
+ expect(user.click).toHaveBeenCalledWith(expect.any(SVGSVGElement));
+ });
+
+ it("allows passing custom userEvent option with setup", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+
+ const userSelectEvent = selectEvent.setup(user);
+
+ const { input } = renderForm(
+
+ );
+
+ await userSelectEvent.clearFirst(input);
+
+ expect(user.click).toHaveBeenCalledWith(expect.any(SVGSVGElement));
+ });
+});
+
+describe("clearAll", () => {
+ it("clears all items", async () => {
+ const { form, input } = renderForm(
+
+ );
+ expect(form).toHaveFormValues({
+ food: ["chocolate", "vanilla", "strawberry"],
});
+
+ await selectEvent.clearAll(input);
+ expect(form).toHaveFormValues({ food: "" });
+ });
+
+ it("allows passing custom userEvent option", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+
+ const { input } = renderForm(
+
+ );
+
+ await selectEvent.clearAll(input, { user });
+
+ expect(user.click).toHaveBeenCalledWith(expect.any(SVGSVGElement));
+ });
+
+ it("allows passing custom userEvent option with setup", async () => {
+ const user = userEvent.setup();
+ jest.spyOn(user, "click");
+
+ const userSelectEvent = selectEvent.setup(user);
+
+ const { input } = renderForm(
+
+ );
+
+ await userSelectEvent.clearAll(input);
+
+ expect(user.click).toHaveBeenCalledWith(expect.any(SVGSVGElement));
});
});
diff --git a/src/act-compat.ts b/src/act-compat.ts
deleted file mode 100644
index 4c9e96a..0000000
--- a/src/act-compat.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * A simple compatibility method for react's "act".
- * If a recent version of @testing-library/react is already installed,
- * we just use their implementation - it's complete and has useful warnings.
- * Otherwise, we just default to a noop.
- *
- * We need this because react-select-event doesn't actually pin a
- * dependency version for @testing-library/react!
- */
-
-type Callback = () => Promise | void | undefined;
-type AsyncAct = (callback: Callback) => Promise;
-type SyncAct = (callback: Callback) => void;
-
-let act: AsyncAct | SyncAct;
-
-try {
- act = require("@testing-library/react").act;
-} catch (_) {
- // istanbul ignore next
- act = (callback: Function) => {
- callback();
- };
-}
-
-export default act;
diff --git a/src/index.ts b/src/index.ts
index 5a91cca..903180b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,11 +4,9 @@ import {
Matcher,
findAllByText,
findByText,
- fireEvent,
waitFor,
} from "@testing-library/dom";
-
-import act from "./act-compat";
+import userEvent from "@testing-library/user-event";
// find the react-select container from its input field 🤷
function getReactSelectContainerFromInput(input: HTMLElement): HTMLElement {
@@ -16,36 +14,42 @@ function getReactSelectContainerFromInput(input: HTMLElement): HTMLElement {
.parentNode as HTMLElement;
}
+type User = ReturnType | typeof userEvent;
+
+type UserEventOptions = {
+ user?: User;
+};
+
/**
* Utility for opening the select's dropdown menu.
* @param {HTMLElement} input The input field (eg. `getByLabelText('The label')`)
*/
-export const openMenu = (input: HTMLElement) => {
- fireEvent.focus(input);
- fireEvent.keyDown(input, {
- key: "ArrowDown",
- keyCode: 40,
- code: 40,
- });
+export const openMenu = async (
+ input: HTMLElement,
+ { user = userEvent }: UserEventOptions = {}
+) => {
+ await user.click(input);
+ await user.type(input, "{ArrowDown}");
};
// type text in the input field
-const type = (input: HTMLElement, text: string) => {
- fireEvent.change(input, { target: { value: text } });
+const type = async (
+ input: HTMLElement,
+ text: string,
+ { user }: Required
+) => {
+ await user.type(input, text);
};
// press the "clear" button, and reset various states
-const clear = async (input: HTMLElement, clearButton: Element) => {
- await act(async () => {
- fireEvent.mouseDown(clearButton);
- fireEvent.click(clearButton);
- // react-select will prevent the menu from opening, and asynchronously focus the select field...
- await waitFor(() => {});
- input.blur();
- });
+const clear = async (
+ clearButton: Element,
+ { user }: Required
+) => {
+ await user.click(clearButton);
};
-interface Config {
+interface Config extends UserEventOptions {
/** A container where the react-select dropdown gets rendered to.
* Useful when rendering the dropdown in a portal using `menuPortalTarget`.
*/
@@ -64,7 +68,7 @@ interface Config {
export const select = async (
input: HTMLElement,
optionOrOptions: Matcher | Array,
- config: Config = {}
+ { user = userEvent, ...config }: Config = {}
) => {
const options = Array.isArray(optionOrOptions)
? optionOrOptions
@@ -72,7 +76,7 @@ export const select = async (
// Select the items we care about
for (const option of options) {
- await openMenu(input);
+ await openMenu(input, { user });
let container;
if (typeof config.container === "function") {
@@ -92,17 +96,15 @@ export const select = async (
ignore: "[aria-live] *,[style*='visibility: hidden']",
});
- act(() => {
- // When the target option is already selected, the react-select display text
- // will also match the selector. In this case, the actual dropdown element is
- // positioned last in the DOM tree.
- const optionElement = matchingElements[matchingElements.length - 1];
- fireEvent.click(optionElement);
- });
+ // When the target option is already selected, the react-select display text
+ // will also match the selector. In this case, the actual dropdown element is
+ // positioned last in the DOM tree.
+ const optionElement = matchingElements[matchingElements.length - 1];
+ await user.click(optionElement);
}
};
-interface CreateConfig extends Config {
+interface CreateConfig extends Config, UserEventOptions {
createOptionText?: string | RegExp;
waitForElement?: boolean;
}
@@ -120,15 +122,13 @@ interface CreateConfig extends Config {
export const create = async (
input: HTMLElement,
option: string,
- { waitForElement = true, ...config }: CreateConfig = {}
+ { waitForElement = true, user = userEvent, ...config }: CreateConfig = {}
) => {
const createOptionText = config.createOptionText || /^Create "/;
- openMenu(input);
- type(input, option);
+ await openMenu(input, { user });
+ await type(input, option, { user });
- fireEvent.change(input, { target: { value: option } });
-
- await select(input, createOptionText, config);
+ await select(input, createOptionText, { ...config, user });
if (waitForElement) {
await findByText(getReactSelectContainerFromInput(input), option);
@@ -139,25 +139,44 @@ export const create = async (
* Utility for clearing the first value of a `react-select` dropdown.
* @param {HTMLElement} input The input field (eg. `getByLabelText('The label')`)
*/
-export const clearFirst = async (input: HTMLElement) => {
+export const clearFirst = async (
+ input: HTMLElement,
+ { user = userEvent }: UserEventOptions = {}
+) => {
const container = getReactSelectContainerFromInput(input);
// The "clear" button is the first svg element that is hidden to screen readers
const clearButton = container.querySelector('svg[aria-hidden="true"]')!;
- await clear(input, clearButton);
+ await clear(clearButton, { user });
};
/**
* Utility for clearing all values in a `react-select` dropdown.
* @param {HTMLElement} input The input field (eg. `getByLabelText('The label')`)
*/
-export const clearAll = async (input: HTMLElement) => {
+export const clearAll = async (
+ input: HTMLElement,
+ { user = userEvent }: UserEventOptions = {}
+) => {
const container = getReactSelectContainerFromInput(input);
// The "clear all" button is the penultimate svg element that is hidden to screen readers
// (the last one is the dropdown arrow)
const elements = container.querySelectorAll('svg[aria-hidden="true"]');
const clearAllButton = elements[elements.length - 2];
- await clear(input, clearAllButton);
+ await clear(clearAllButton, { user });
};
+const setup = (user: User): typeof selectEvent => ({
+ select: (...params: Parameters) =>
+ select(params[0], params[1], { user, ...params[2] }),
+ create: (...params: Parameters) =>
+ create(params[0], params[1], { user, ...params[2] }),
+ clearFirst: (...params: Parameters) =>
+ clearFirst(params[0], { user, ...params[1] }),
+ clearAll: (...params: Parameters) =>
+ clearAll(params[0], { user, ...params[1] }),
+ openMenu: (...params: Parameters) =>
+ openMenu(params[0], { user, ...params[1] }),
+});
+
const selectEvent = { select, create, clearFirst, clearAll, openMenu };
-export default selectEvent;
+export default { ...selectEvent, setup };