Как проверить реагировать на выбор с помощью библиотеки реагирования на тестирование

App.js

import React, { Component } from "react";
import Select from "react-select";

const SELECT_OPTIONS = ["FOO", "BAR"].map(e => {
  return { value: e, label: e };
});

class App extends Component {
  state = {
    selected: SELECT_OPTIONS[0].value
  };

  handleSelectChange = e => {
    this.setState({ selected: e.value });
  };

  render() {
    const { selected } = this.state;
    const value = { value: selected, label: selected };
    return (
      <div className="App">
        <div data-testid="select">
          <Select
            multi={false}
            value={value}
            options={SELECT_OPTIONS}
            onChange={this.handleSelectChange}
          />
        </div>
        <p data-testid="select-output">{selected}</p>
      </div>
    );
  }
}

export default App;

App.test.js

import React from "react";
import {
  render,
  fireEvent,
  cleanup,
  waitForElement,
  getByText
} from "react-testing-library";
import App from "./App";

afterEach(cleanup);

const setup = () => {
  const utils = render(<App />);
  const selectOutput = utils.getByTestId("select-output");
  const selectInput = document.getElementById("react-select-2-input");
  return { selectOutput, selectInput };
};

test("it can change selected item", async () => {
  const { selectOutput, selectInput } = setup();
  getByText(selectOutput, "FOO");
  fireEvent.change(selectInput, { target: { value: "BAR" } });
  await waitForElement(() => getByText(selectOutput, "BAR"));
});

Этот минимальный пример работает, как и ожидалось, в браузере, но тест не пройден. Я думаю, что обработчик onChange в не вызывается. Как я могу вызвать обратный вызов onChange в тесте? Каков предпочтительный способ найти элемент для fireEvent? Спасибо

13 ответов

Решение

Это должен быть самый часто задаваемый вопрос о RTL:D

Лучшая стратегия заключается в использовании jest.mock (или эквивалент в вашей структуре тестирования), чтобы смоделировать выборку и визуализировать выборку HTML вместо этого.

Для получения дополнительной информации о том, почему это лучший подход, я написал кое-что, что относится и к этому делу. ОП спросил о выборе в Material-UI, но идея та же.

Оригинальный вопрос и мой ответ:

Потому что вы не можете контролировать этот интерфейс. Это определено в стороннем модуле.

Итак, у вас есть два варианта:

Вы можете выяснить, какой HTML создает библиотека материалов, а затем использовать container.querySelector, чтобы найти его элементы и взаимодействовать с ним. Это займет некоторое время, но это должно быть возможно. После того, как вы сделали все это, вы должны надеяться, что при каждом новом выпуске они не слишком изменяют структуру DOM, или вам, возможно, придется обновить все ваши тесты.

Другой вариант - довериться тому, что Material-UI сделает компонент, который работает, и который могут использовать ваши пользователи. Основываясь на этом доверии, вы можете просто заменить этот компонент в своих тестах на более простой.

Да, первый вариант проверяет то, что видит пользователь, но второй вариант легче поддерживать.

По моему опыту, второй вариант вполне подходит, но, конечно, ваш вариант использования может отличаться, и вам, возможно, придется протестировать реальный компонент.

Это пример того, как вы могли бы посмеяться над выбором:

jest.mock("react-select", () => ({ options, value, onChange }) => {
  function handleChange(event) {
    const option = options.find(
      option => option.value === event.currentTarget.value
    );
    onChange(option);
  }
  return (
    <select data-testid="select" value={value} onChange={handleChange}>
      {options.map(({ label, value }) => (
        <option key={value} value={value}>
          {label}
        </option>
      ))}
    </select>
  );
});

Вы можете прочитать больше здесь.

В моем проекте я использую реагирующую библиотеку и jest-dom. Я столкнулся с той же проблемой - после некоторого расследования я нашел решение, основанное на теме: https://github.com/airbnb/enzyme/issues/400

Обратите внимание, что функция верхнего уровня для рендеринга должна быть асинхронной, а также отдельными шагами.

В этом случае нет необходимости использовать событие фокуса, и это позволит выбрать несколько значений.

Также внутри getSelectItem должен быть асинхронный обратный вызов.

const DOWN_ARROW = { keyCode: 40 };

it('renders and values can be filled then submitted', async () => {
  const {
    asFragment,
    getByLabelText,
    getByText,
  } = render(<MyComponent />);

  ( ... )

  // the function
  const getSelectItem = (getByLabelText, getByText) => async (selectLabel, itemText) => {
    fireEvent.keyDown(getByLabelText(selectLabel), DOWN_ARROW);
    await waitForElement(() => getByText(itemText));
    fireEvent.click(getByText(itemText));
  }

  // usage
  const selectItem = getSelectItem(getByLabelText, getByText);

  await selectItem('Label', 'Option');

  ( ... )

}

Наконец, есть библиотека, которая нам в этом помогает: https://testing-library.com/docs/ecosystem-react-select-event. Отлично работает как для одиночного выбора, так и для множественного выбора:

Из @testing-library/react документы:

import React from 'react'
import Select from 'react-select'
import { render } from '@testing-library/react'
import selectEvent from 'react-select-event'

const { getByTestId, getByLabelText } = render(
  <form data-testid="form">
    <label htmlFor="food">Food</label>
    <Select options={OPTIONS} name="food" inputId="food" isMulti />
  </form>
)
expect(getByTestId('form')).toHaveFormValues({ food: '' }) // empty select

// select two values...
await selectEvent.select(getByLabelText('Food'), ['Strawberry', 'Mango'])
expect(getByTestId('form')).toHaveFormValues({ food: ['strawberry', 'mango'] })

// ...and add a third one
await selectEvent.select(getByLabelText('Food'), 'Chocolate')
expect(getByTestId('form')).toHaveFormValues({
  food: ['strawberry', 'mango', 'chocolate'],
})

Спасибо https://github.com/romgain/react-select-event за такой замечательный пакет!

Подобно ответу @momimomo, я написал небольшую помощницу, чтобы выбрать вариант из react-select в TypeScript.

Файл-помощник:

import { getByText, findByText, fireEvent } from '@testing-library/react';

const keyDownEvent = {
    key: 'ArrowDown',
};

export async function selectOption(container: HTMLElement, optionText: string) {
    const placeholder = getByText(container, 'Select...');
    fireEvent.keyDown(placeholder, keyDownEvent);
    await findByText(container, optionText);
    fireEvent.click(getByText(container, optionText));
}

Применение:

export const MyComponent: React.FunctionComponent = () => {
    return (
        <div data-testid="day-selector">
            <Select {...reactSelectOptions} />
        </div>
    );
};
it('can select an option', async () => {
    const { getByTestId } = render(<MyComponent />);
    // Open the react-select options then click on "Monday".
    await selectOption(getByTestId('day-selector'), 'Monday');
});

Простой способ проверить это сделать то, что должен делать пользователь.

  • Нажмите на поле выбора.
  • Нажмите на один из пунктов в раскрывающемся списке.
      function CustomSelect() {

  const colourOptions = [
    { value: 'orange', label: 'Orange', color: '#FF8B00' },
    { value: 'yellow', label: 'Yellow', color: '#FFC400' }
  ]

  return <Select 
    aria-label="my custom select" 
    options={colourOptions}
    //... props  
  />
}
      import { act, render, screen } from '@testing-library/react'; 
import userEvent from '@testing-library/user-event';
// another imports

test('show selected item...', async () => {
  const { getByText, getByLabelText } = render(<CustomSelect />);

  expect(getByText('Orange')).not.toBeInTheDocument();
  
  const myCustomSelect = getByLabelText(/my custom select/i);
  await act(async () => userEvent.click(myCustomSelect));

  const selectedItem = getByText('Orange');
  await act(async () => userEvent.click(selectedItem));

  expect(getByText('Orange')).toBeInTheDocument();
});

Если вы не используете label элемент, способ пойти с react-select-event является:

      const select = screen.container.querySelector(
  "input[name='select']"
);

selectEvent.select(select, "Value");

Это решение сработало для меня.

fireEvent.change(getByTestId("select-test-id"), { target: { value: "1" } });

Надеюсь, это поможет борцам.

Потому что я хотел протестировать компонент, который обертывал react-select, издеваясь над ним с помощью обычного <select>элемент не сработал бы. Так что в итоге я использовал тот же подход, что и собственные тесты пакета , который предоставляет classNameв реквизите, а затем использовать его с querySelector()для доступа к отображаемому элементу в тесте:

      const BASIC_PROPS: BasicProps = {
  className: 'react-select',
  classNamePrefix: 'react-select', 
  // ...
};

let { container } = render(
  <Select {...props} menuIsOpen escapeClearsValue isClearable />
);
fireEvent.keyDown(container.querySelector('.react-select')!, {
  keyCode: 27,
  key: 'Escape',
});
expect(
  container.querySelector('.react-select__single-value')!.textContent
).toEqual('0');

В моем случае я использовал реакцию-выбор с библиотекой реагирования-тестирования и редукционной формой, сталкивался с различными проблемами в разных методах, которые я пробовал, единственный, который сработал:

        // target the <input/> of your element
  const dropdown = screen.getByTestId('my-select-test-id').querySelector('input')

  // opens up the options, notice the "mousedown" event
  fireEvent(
    dropdown,
    new MouseEvent('mousedown', {
      bubbles: true,
      cancelable: true,
    })
  )

  const dropdownOption = await screen.findByText('my option text')

  // selects the option we want, notice again the "mousedown" event
  fireEvent(
    dropdownOption,
    new MouseEvent('mousedown', {
      bubbles: true,
      cancelable: true,
    })
  )
export async function selectOption(container: HTMLElement, optionText: string) {
  let listControl: any = '';
  await waitForElement(
    () => (listControl = container.querySelector('.Select-control')),
  );
  fireEvent.mouseDown(listControl);
  await wait();
  const option = getByText(container, optionText);
  fireEvent.mouseDown(option);
  await wait();
}

ПРИМЕЧАНИЕ: контейнер: контейнер для поля выбора (например: container = getByTestId('seclectTestId'))

если по какой-либо причине есть метка с таким же именем, используйте это

      const [firstLabel, secondLabel] = getAllByLabelText('State');
    await act(async () => {
      fireEvent.focus(firstLabel);
      fireEvent.keyDown(firstLabel, {
        key: 'ArrowDown',
        keyCode: 40,
        code: 40,
      });

      await waitFor(() => {
        fireEvent.click(getByText('Alabama'));
      });

      fireEvent.focus(secondLabel);
      fireEvent.keyDown(secondLabel, {
        key: 'ArrowDown',
        keyCode: 40,
        code: 40,
      });

      await waitFor(() => {
        fireEvent.click(getByText('Alaska'));
      });
    });

или Если у вас есть способ запросить свой раздел — например, с помощью data-testid— вы можете использовать внутри:

      within(getByTestId('id-for-section-A')).getByLabelText('Days')
within(getByTestId('id-for-section-B')).getByLabelText('Days')

Альтернативное решение, которое сработало для моего варианта использования и не требует имитации с выбором реакции или отдельной библиотеки (благодаря @Steve Vaughan), найденное в чате спектра библиотеки тестирования реакции.

Обратной стороной является то, что мы должны использовать container.querySelector RTL не рекомендует использовать более гибкие селекторы.

Для всех там - я получил свой выбор, выполнив fireEvent.mouseDown для опции вместо щелчка.

Другие вопросы по тегам