// Based on https://github.com/romgain/react-select-event // Switched from fireEvent to userEvent to avoid act warnings in react 18 // Copyright 2019 Romain Bertrand // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. import userEvent from '@testing-library/user-event'; // eslint-disable-line no-restricted-imports import {findAllByText, findByText, type Matcher, waitFor} from './reactTestingLibrary'; /** * Find the react-select container from its input field */ function getReactSelectContainerFromInput(input: HTMLElement): HTMLElement { return input.parentNode!.parentNode!.parentNode!.parentNode!.parentNode as HTMLElement; } type User = ReturnType | typeof userEvent; type UserEventOptions = { user?: User; }; /** * Open the select's dropdown menu. * @param input The input field (eg. `getByLabelText('The label')`) */ const openMenu = async ( input: HTMLElement, {user = userEvent}: UserEventOptions = {} ) => { await user.click(input, {skipHover: true}); // Arrow down may be required? // await user.type(input, '{ArrowDown}'); }; /** * Type text in the input field */ 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 (clearButton: Element, {user}: Required) => { await user.click(clearButton, {skipHover: true}); }; interface Config extends UserEventOptions { /** * A container where the react-select dropdown gets rendered to. * Useful when rendering the dropdown in a portal using `menuPortalTarget`. * Can be specified as a function if it needs to be lazily evaluated. */ container?: HTMLElement | (() => HTMLElement); } /** * Utility for selecting a value in a `react-select` dropdown. * @param input The input field (eg. `getByLabelText('The label')`) * @param optionOrOptions The display name(s) for the option(s) to select */ const select = async ( input: HTMLElement, optionOrOptions: Matcher | Array, {user = userEvent, ...config}: Config = {} ) => { const options = Array.isArray(optionOrOptions) ? optionOrOptions : [optionOrOptions]; // Select the items we care about for (const option of options) { await openMenu(input, {user}); let container: HTMLElement; if (typeof config.container === 'function') { // when specified as a function, the container needs to be lazily evaluated, so // we have to wait for it to be visible: await waitFor(config.container); container = config.container(); } else if (config.container) { container = config.container; } else { container = getReactSelectContainerFromInput(input); } // only consider visible, interactive elements const matchingElements = await findAllByText(container, option, { ignore: "[aria-live] *,[style*='visibility: hidden']", }); // 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, {skipHover: true}); } }; interface CreateConfig extends Config, UserEventOptions { /** * Custom label for the "create new ..." option in the menu (string or regexp) */ createOptionText?: string | RegExp; /** * Whether create should wait for new option to be populated in the select container */ waitForElement?: boolean; } /** * Creates and selects a value in a Creatable `react-select` dropdown. * @param input The input field (eg. `getByLabelText('The label')`) * @param option The display name for the option to type and select */ const create = async ( input: HTMLElement, option: string, {waitForElement = true, user = userEvent, ...config}: CreateConfig = {} ) => { const createOptionText = config.createOptionText || /^Create "/; await openMenu(input, {user}); await type(input, option, {user}); await select(input, createOptionText, {...config, user}); if (waitForElement) { await findByText(getReactSelectContainerFromInput(input), option); } }; /** * Clears the first value of a `react-select` dropdown. * @param input The input field (eg. `getByLabelText('The label')`) */ 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(clearButton, {user}); }; /** * Clears all values in a `react-select` dropdown. * @param input The input field (eg. `getByLabelText('The label')`) */ 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(clearAllButton, {user}); }; const selectEvent = {select, create, clearFirst, clearAll, openMenu}; export default selectEvent;