123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- // 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.setup> | 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<UserEventOptions>
- ) => {
- await user.type(input, text);
- };
- /**
- * Press the "clear" button, and reset various states
- */
- const clear = async (clearButton: Element, {user}: Required<UserEventOptions>) => {
- 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<Matcher>,
- {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;
|