Тестирование компонента React Select

https://github.com/JedWatson/react-select

Я хотел бы использовать React-Select реагирующий компонент, но мне нужно добавить тесты.

Я пробовал несколько вариантов, которые я нашел с Google, но, похоже, ничего не работает. У меня есть код ниже, но он не вызывает событие изменения. Мне удалось добавить событие фокуса, которое добавляет класс "is-focus", но класс "is-open" все еще отсутствует.

Я использовал: https://github.com/JedWatson/react-select/blob/master/test/Select-test.js в качестве ссылки

Я пытался использовать событие изменения только в поле ввода, но это тоже не помогло. Я заметил, что есть onInputChange={this.change} для избранных.

Тестовое задание

import Home from '../../src/components/home';
import { mount } from 'enzyme'

describe('Home', () => {

it("renders home", () => {

    const component = mount(<Home/>);

    // default class on .Select div
    // "Select foobar Select--single is-searchable"

    const select = component.find('.Select');

    // After focus event
    // "Select foobar Select--single is-searchable is-focussed"
    // missing is-open
    TestUtils.Simulate.focus(select.find('input'));

    //this is not working
    TestUtils.Simulate.keyDown(select.find('.Select-control'), { keyCode: 40, key: 'ArrowDown' });
    TestUtils.Simulate.keyDown(select.find('.Select-control'), { keyCode: 13, key: 'Enter' });

    // as per code below I expect the h2 to have the select value in it eg 'feaure'

});
});

Тестируемый компонент

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

class Home extends Component {
constructor(props) {
    super(props);

    this.state = {
        message: "Please select option"};
    this.change = this.change.bind(this);
}

change(event) {

    if(event.value) {
        this.setState({message: event.label});
    }
}

render () {

    const options = [ {label: 'bug', value: 1} , {label: 'feature', value: 2 }, {label: 'documents', value: 3}, {label: 'discussion', value: 4}];

    return (
      <div className='content xs-full-height'>
          <div>
              <h2>{this.state.message}</h2>

              <Select
                  name="select"
                  value={this.state.message}
                  options={options}
                  onInputChange={this.change}
                  onChange={this.change}
              />

          </div>
        </div>
    );
}
}

export default Home;

Командная строка Для запуска теста я делаю:

>> npm run test

и в package.js у меня есть этот скрипт:

"test": "mocha --compilers js:babel-core/register -w test/browser.js ./new",

Испытательная установка

и browser.js это:

import 'babel-register';
import jsdom from 'jsdom';

const exposedProperties = ['window', 'navigator', 'document'];

global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
Object.keys(document.defaultView).forEach((property) => {
   if (typeof global[property] === 'undefined') {
       exposedProperties.push(property);
       global[property] = document.defaultView[property];
   }
});

global.navigator = {
    userAgent: 'node.js'
};

Я также попытался использовать методы тестирования, описанные здесь: https://github.com/StephenGrider/ReduxSimpleStarter

Любая помощь будет оценена

17 ответов

Это повторяющийся вопрос. Я делюсь своим кодом со 100% прохождением тестов, которые покрывают 100% моего исходного кода.

Мой компонент выглядит так

MySelectComponent({ options, onChange }) {

  return <div data-testid="my-select-component">
    <Select
      className="basic-single"
      classNamePrefix="select"
      name="myOptions"
      placeholder="Select an option"
      options={options}
      onChange={e => onChange(e)}
    />
</div>;
}

Причина, по которой я добавляю оболочку на свой Select с участием data-testid="my-select-component" заключается в том, что на нем будет доступен отображаемый элемент параметров, иначе я не могу проверить, существует ли текстовый параметр (вы поймете лучше, когда увидите мои тесты).

Это рабочий пример, и при рендеринге он покажет компонент выбора с 10 вариантами.

Тест 1: должен отображаться без ошибок

  • Я визуализирую компонент.

  • Я ищу заполнитель.

Тест 2: должен вызывать onChange при выборе первого варианта

  • Я визуализирую компонент.

  • Я проверяю, если мой mockedOnChange еще не называется.

  • Смоделировать ArrowDown событие.

  • Щелкните по первому варианту.

  • Я проверяю, если mockedOnChange вызывается 1 раз с меткой и значением 1-й опции.

Тест 3: должен вызывать onChange, когда выбран первый вариант, затем второй вариант, затем 9-й.

  • Я визуализирую компонент.

  • Я моделирую выбор первого варианта.

  • Имитирую выбор 2-го варианта.

  • Имитирую выбор 9-го варианта.

  • Я проверяю, есть ли mockedOnChange is called 3 times with the 9th option bale and value.

Test 4: should call onChange when filtering by input value

  • I render the component.

  • I simulate a change on the input field by typing "option 1".

  • I know, based on my mockedOptions that the filtered result will be "Mocked option 1" and "Mocked option 10".

  • I simulate 2 ArrowDown events.

  • I check that the mockedOnChange is called with the 2nd filtered option with right label and value.

Complete test file

import React from 'react';
import { render, fireEvent, cleanup, waitForElement } from '@testing-library/react';
import MySelectComponent from './MySelectComponent';

afterEach(cleanup);

describe ('Test react-select component', () => {

    const mockedOptions = [
        {label: 'Mocked option 1', value: 'mocked-option-1'},
        {label: 'Mocked option 2', value: 'mocked-option-2'},
        {label: 'Mocked option 3', value: 'mocked-option-3'},
        {label: 'Mocked option 4', value: 'mocked-option-4'},
        {label: 'Mocked option 5', value: 'mocked-option-5'},
        {label: 'Mocked option 6', value: 'mocked-option-6'},
        {label: 'Mocked option 7', value: 'mocked-option-7'},
        {label: 'Mocked option 8', value: 'mocked-option-8'},
        {label: 'Mocked option 9', value: 'mocked-option-9'},
        {label: 'Mocked option 10', value: 'mocked-option-10'},
    ];

    it('should render without errors', async () => {
        const mockedOnChange = jest.fn();
        const { getByText } = render(<MySelectComponent 
            options={mockedOptions} 
            onChange={mockedOnChange} />);

        const placeholder = getByText('Select an option');

        expect(placeholder).toBeTruthy();
    });

    it('should call onChange when the first option is selected', async () => {
        const mockedOnChange = jest.fn();
        const { getByText, queryByTestId } = render(<MySelectComponent 
            options={mockedOptions} 
            onChange={mockedOnChange} />);

        const mySelectComponent = queryByTestId('my-select-component');

        expect(mySelectComponent).toBeDefined();
        expect(mySelectComponent).not.toBeNull();
        expect(mockedOnChange).toHaveBeenCalledTimes(0);

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 1'));
        fireEvent.click(getByText('Mocked option 1'));

        expect(mockedOnChange).toHaveBeenCalledTimes(1);
        expect(mockedOnChange).toHaveBeenCalledWith({label: 'Mocked option 1', value: 'mocked-option-1'});

    });

    it('should call onChange when the first option is selected then second option then the 9th one', async () => {
        const mockedOnChange = jest.fn();
        const { getByText, queryByTestId } = render(<MySelectComponent 
            options={mockedOptions} 
            onChange={mockedOnChange} />);

        const mySelectComponent = queryByTestId('my-select-component');

        expect(mySelectComponent).toBeDefined();
        expect(mySelectComponent).not.toBeNull();
        expect(mockedOnChange).toHaveBeenCalledTimes(0);

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 1'));
        fireEvent.click(getByText('Mocked option 1'));

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 2'));
        fireEvent.click(getByText('Mocked option 2'));

        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });
        await waitForElement(() => getByText('Mocked option 9'));
        fireEvent.click(getByText('Mocked option 9'));

        expect(mockedOnChange).toHaveBeenCalledTimes(3);
        expect(mockedOnChange).toHaveBeenCalledWith({label: 'Mocked option 9', value: 'mocked-option-9'});
    });

    it('should call onChange when filtering by input value', async () => {
      const mockedOnChange = jest.fn();
      const { getByText, queryByTestId, container } = render(<MySelectComponent 
        options={mockedOptions} 
        onChange={mockedOnChange} />);

        const mySelectComponent = queryByTestId('my-select-component');

        fireEvent.change(container.querySelector('input'), {
            target: { value: 'option 1' },
        });

        // select Mocked option 1
        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });  
        // select Mocked option 10
        fireEvent.keyDown(mySelectComponent.firstChild, { key: 'ArrowDown' });

        await waitForElement(() => getByText('Mocked option 10'));
        fireEvent.click(getByText('Mocked option 10'));

        expect(mockedOnChange).toHaveBeenCalledTimes(1);
        expect(mockedOnChange).toHaveBeenCalledWith({label: 'Mocked option 10', value: 'mocked-option-10'});
    });

});

I hope that this help.

Я попробовал оба ответа, перечисленных выше, и до сих пор не повезло.

Что сработало для меня:

  1. добавлять classNamePrefix опора - то есть list (как указано в других ответах):

    <Select classNamePrefix='list' options={[ { label: 'one', value: 'one' }, { label: 'two', value: 'two' } ]}/>

  2. выберите индикатор выпадающего меню и смоделируйте mouseDown => открытый раскрывающийся список:

    wrapper.find('.list__dropdown-indicator').simulate('mouseDown', { button: 0 });

  3. ожидать, что что-то произойдет, т.е. в моем случае я проверял количество выпадающих опций

    expect(wrapper.find('.list__option').length).toEqual(2);

Если у вас есть контроль над отправкой реквизита, вы можете добавить menuIsOpen Пропустить, чтобы меню всегда было открыто (он же шаг 2 в списке).

Чтобы выбрать значение из раскрывающегося списка, после открытия раскрывающегося списка:

wrapper.find('.list__option').last().simulate('click', null);

тогда вы можете проверить либо:

expect(wrapper.find('.list__value-container').text()).toEqual('two');

или же

expect(wrapper.find('.list__single-value').text()).toEqual('two');

С https://github.com/JedWatson/react-select/issues/1357

Единственное решение, которое я нашел, состояло в том, чтобы смоделировать выбор через события нажатия клавиши:

wrapper.find('.Select-control').simulate('keyDown', { keyCode: 40 });
// you can use 'input' instead of '.Select-control'
wrapper.find('.Select-control').simulate('keyDown', { keyCode: 13 });
expect(size).to.eql('your first value in the list')

Использование библиотеки тестирования и v2.0

Попытка избежать использования чего-то очень конкретного, например classNamePrefix или взломать способ работы компонента, ища опору onChange или что-то еще.

const callback = jest.fn();
const { container, getByText} = render(<Select ... onChange={callback} />);

Теперь мы в основном притворяемся скринридером, фокусируемся и нажимаем стрелку вниз.

fireEvent.focus(container.querySelector('input'));
fireEvent.keyDown(container.querySelector('input'), { key: 'ArrowDown', code: 40 });

А теперь нажмите на желаемое значение

fireEvent.click(getByText('Option Two'));

И отстаивать.

expect(callback).toHaveBeenCalledWith({ value: 'two', label: 'Option Two'});

Чтобы добавить к тому, что сказал Кит, использование метода имитации, кажется, является единственным способом реализовать функциональность. Однако, когда я попробовал это в своем решении, это не сработало - я использую Typescript, хотя и не уверен, имеет ли это отношение, но я обнаружил, что необходимо также передать свойство key при моделировании события:

wrapper.find('.Select-control').simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });
wrapper.find('.Select-control').simulate('keyDown', { key: 'Enter', keyCode: 13 });

Я также обнаружил, что установка свойства classNamePrefix значительно упростила проведение простого теста, чтобы дать мне уверенность в том, что компонент правильно реагировал на смоделированные события. При установке этого префикса полезные части компонента украшаются именами классов, обеспечивающими легкий доступ к ним (вы можете определить эти полезные имена классов, проверив элементы в инструментах Google Dev). Простой тест Jest:

it('react-select will respond to events correctly', () => {
    const sut = Enzyme.mount(
    <Select 
        classNamePrefix="list" 
        options={[{ label: 'item 1', value: 1 }]}
    />);

    // The intereactive element uses the suffix __control **note there are two underscores** 
    sut.find('.list__control').first().simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });
    sut.find('.list__control').first().simulate('keyDown', { key: 'Enter', keyCode: 13 });

    // the selected value uses the suffix __single-value **note there are two underscores** 
    expect(sut.find('.list__single-value').first().text()).toEqual('item 1');
});

Для более новой версии реакции-выбора (2.x+), вышеупомянутый метод не работает, потому что реакция-выбора использует эмоцию. Таким образом, wrapper.find('.Select-control') или же wrapper.find('.list__option') больше не работает. реакция-выберите 2.x + оборачивает Select компонент внутри государственного менеджера, но вы можете получить доступ к onChange метод Select составная часть. Я использую следующий код для запуска выбора:

wrapper.find(Select).props().onChange({ value: ... })

Если кто-то использует энзим, это работает для меня:

  wrapper.find('Select').simulate('change', {
    target: { name: 'select', value: 1 },
  });

где "select" - это имя атрибута, как определено здесь:

  <Select
      name="select"
      ...

и "значение" является значением желаемой опции.

Просто хочу добавить, я на самом деле должен был добавить classNamePrefix опора для Select компонент в противном случае я не получил *__control классы, чтобы захватить.

С реактивной тестовой библиотекой:

<Select id='myId' onChange=(list: ReactSelectOption[]) => {
                        props.changeGroupItem(
                            {
                                items: list ? list.map((item) => item.value) : [],
                            }
                        );
                    }
/>

А потом в моем тесте

const selectInput = container.querySelector(`#myId input`) as HTMLInputElement;
    fireEvent.focus(selectInput);
    fireEvent.mouseDown(selectInput);
    fireEvent.click(getByText("myValue"));
    expect(props.changeGroupItem).toHaveBeenCalledWith(
        {
            items: ["myDefaultValue", "myValue"],
        }
    );

У меня была такая же проблема, чтобы протестировать реакцию на выбор, поэтому мое решение было таким.

мой компонент реакции-выбора:

            <Select
      options={options}
      placeholder="Select an Option"
    />

мой тест:

        it('should select an option', () => {
    const { getByText } = render(
      <MySelect/>
    );

    fireEvent.focus(getByText('Select an Option'));

    fireEvent.keyDown(getByText('Select an Option'), {
      key: 'ArrowDown',
      code: 40,
    });

    fireEvent.click(getByText("my wanted option"));
    expect(getByText("my wanted option")).toBeDefined();
  }

Существует библиотека для имитации пользовательских событий на элементах response-select для использования с react-testing-library. Работает с react select 2+.

https://www.npmjs.com/package/react-select-event

Вот так:

const { getByRole, getByLabelText } = render(
  <form role="form">
    <label htmlFor="food">Food</label>
    <Select options={OPTIONS} name="food" inputId="food" isMulti />
  </form>
);
expect(getByRole("form")).toHaveFormValues({ food: "" });
 
await selectEvent.select(getByLabelText("Food"), ["Strawberry", "Mango"]);
expect(getByRole("form")).toHaveFormValues({ food: ["strawberry", "mango"] });
 
await selectEvent.select(getByLabelText("Food"), "Chocolate");
expect(getByRole("form")).toHaveFormValues({
  food: ["strawberry", "mango", "chocolate"],
});

Я пробовал все решения здесь - у меня ничего не получалось.

Мне удалось решить проблему с библиотекой пользовательских событий .Оформить заказ selectOptionsфункция.

Для тех, кто использует фермент v3.11.0 и react-select v3.0.8, у меня это сработало.

component.find('Select').simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });

Вот мой Select. Я использую его вместе сredux-form

<Select
    {...input}
    styles={customStyles}
    options={props.options}
    formatGroupLabel={formatGroupLabel}
    placeholder="Custom Search..."
    isSearchable={true}
    onBlur={handleBlur}
/>

Образец тестов

it('should render dropdown on keyDown', () => {
    expect(component.find('MenuList').length).toEqual(1);
});

it('should render the correct amount of options', () => {
    expect(component.find('Option').length).toEqual(optionsLength);
});
wrapper.find(ReactSelectComponent.instance().onChange(...params));

/questions/4628196/testirovanie-komponenta-react-select/55127077#55127077 почти работает для меня. Я только что добавил событие keyDown для открытия меню выбора.

it('my test', () => {
  const { container } = getShallow();
  const elSelector = container.querySelector('.Select-input');

  expect(propsComponent.onPageSizeChange).toHaveBeenCalledTimes(0);

  // open select menu
  fireEvent.keyDown(elSelector, { keyCode: 13 });

  // choose next option
  fireEvent.keyDown(elSelector, { key: 'ArrowDown', code: 40 });

  // send the option
  fireEvent.keyDown(elSelector, { keyCode: 13 });

  expect(propsComponent.onPageSizeChange).toHaveBeenCalledTimes(1);
});

У меня была аналогичная проблема с моей реализацией MUI, реагирующей на компонент Select. Я не смог найти хорошего решения, кроме этого:

        it('changes the state when an option is checked', async () => {
    const result = renderWithContext(
      <div>
        <MySelectImplementation  />
      </div>
    );

    const selectOptions= screen.getByRole('button', {
      name: `${btnText}`,
    });

    await act(async () => {
      await fireEvent.keyDown(selectOptions.firstChild as ChildNode, {
        key: 'ArrowDown',
      });
    });

    await waitFor(() => screen.getByText('Text In My List'));
    const checkBoxes = screen.getAllByRole('checkbox');
// check the first checkbox
    fireEvent.click(checkBoxes[0]);

    expect(
      result.store.getState().optionState
    ).toEqual(['Text In My List']);
  });

Я все еще боролся с той же проблемой, но, потратив на это некоторое время, нашел способ обрабатывать раскрывающийся список реакции-выбора для автоматического тестирования . Решение можно найти здесь .

Стоит отметить, что для этого не требуется никакого дополнительного dom.

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