简体   繁体   English

如何使用 react-testing-library 测试 react-select

[英]how to test react-select with react-testing-library

App.js应用程序.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应用程序.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"));
});

This minimal example works as expected in the browser but the test fails.这个最小的示例在浏览器中按预期工作,但测试失败。 I think the onChange handler in is not invoked.我认为未调用 onChange 处理程序。 How can I trigger the onChange callback in the test?如何在测试中触发 onChange 回调? What is the preferred way to find the element to fireEvent at?查找要触发事件的元素的首选方法是什么? Thank you谢谢

In my project, I'm using react-testing-library and jest-dom.在我的项目中,我使用了 react-testing-library 和 jest-dom。 I ran into same problem - after some investigation I found solution, based on thread: https://github.com/airbnb/enzyme/issues/400我遇到了同样的问题 - 经过一番调查,我找到了解决方案,基于线程: https ://github.com/airbnb/enzyme/issues/400

Notice that the top-level function for render has to be async, as well as individual steps.请注意,渲染的顶级函数必须是异步的,以及各个步骤。

There is no need to use focus event in this case, and it will allow to select multiple values.在这种情况下不需要使用焦点事件,它允许选择多个值。

Also, there has to be async callback inside getSelectItem.此外,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');

  ( ... )

}

This got to be the most asked question about RTL :D这一定是关于 RTL 被问得最多的问题:D

The best strategy is to use jest.mock (or the equivalent in your testing framework) to mock the select and render an HTML select instead.最好的策略是使用jest.mock (或测试框架中的等效项)来模拟选择并呈现 HTML 选择。

For more info on why this is the best approach, I wrote something that applies to this case too.有关为什么这是最好的方法的更多信息,我也写了一些适用于这种情况的内容。 The OP asked about a select in Material-UI but the idea is the same. OP 询问了 Material-UI 中的选择,但想法是一样的。

Original question and my answer: 原始问题和我的答案:

Because you have no control over that UI.因为您无法控制该 UI。 It's defined in a 3rd party module.它在第 3 方模块中定义。

So, you have two options:所以,你有两个选择:

You can figure out what HTML the material library creates and then use container.querySelector to find its elements and interact with it.您可以找出材质库创建的 HTML,然后使用 container.querySelector 查找其元素并与之交互。 It takes a while but it should be possible.这需要一段时间,但应该是可能的。 After you have done all of that you have to hope that at every new release they don't change the DOM structure too much or you might have to update all your tests.完成所有这些之后,您必须希望在每个新版本中它们不会过多地更改 DOM 结构,否则您可能必须更新所有测试。

The other option is to trust that Material-UI is going to make a component that works and that your users can use.另一种选择是相信 Material-UI 将制作一个可以工作并且您的用户可以使用的组件。 Based on that trust you can simply replace that component in your tests for a simpler one.基于这种信任,您可以简单地将测试中的该组件替换为更简单的组件。

Yes, option one tests what the user sees but option two is easier to maintain.是的,选项一测试用户看到的内容,但选项二更容易维护。

In my experience the second option is just fine but of course, your use-case might be different and you might have to test the actual component.以我的经验,第二个选项很好,但当然,您的用例可能会有所不同,您可能必须测试实际的组件。

This is an example of how you could mock a select:这是如何模拟选择的示例:

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>
  );
});

You can read more here .你可以在这里阅读更多。

Finally, there is a library that helps us with that: https://testing-library.com/docs/ecosystem-react-select-event .最后,有一个库可以帮助我们: https ://testing-library.com/docs/ecosystem-react-select-event。 Works perfectly for both single select or select-multiple:适用于单选或多选:

From @testing-library/react docs:来自@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'],
})

Thanks https://github.com/romgain/react-select-event for such an awesome package!感谢https://github.com/romgain/react-select-event提供这么棒的软件包!

Similar to @momimomo's answer, I wrote a small helper to pick an option from react-select in TypeScript.与@momimomo 的回答类似,我写了一个小助手来从 TypeScript 中的react-select中选择一个选项。

Helper file:帮助文件:

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));
}

Usage:用法:

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');
});

This solution worked for me.这个解决方案对我有用。

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

Hope it might help strugglers.希望对奋斗者有所帮助。

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();
}

NOTE: container: container for select box ( eg: container = getByTestId('seclectTestId') )注意:容器:选择框的容器(例如:container = getByTestId('seclectTestId'))

An alternative solution which worked for my use case and requires no react-select mocking or separate library (thanks to @Steve Vaughan ) found on the react-testing-library spectrum chat.一种适用于我的用例的替代解决方案,不需要在react-testing-library 频谱聊天中找到 react-select 模拟或单独的库(感谢@Steve Vaughan )。

The downside to this is we have to use container.querySelector which RTL advises against in favour of its more resillient selectors.这样做的缺点是我们必须使用 RTL 建议不要使用的container.querySelector ,以支持其更具弹性的选择器。

In case you are not using a label element, the way to go with react-select-event is:如果您不使用label元素,则使用react-select-event的方法是:

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

selectEvent.select(select, "Value");

if for whatever reason there is a label with the same name use this如果出于某种原因有一个同名的标签使用这个

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'));
      });
    });

or If you have a way to query your section—for example with a data-testid—you could use within:或者如果您有办法查询您的部分 - 例如使用 data-testid - 您可以在以下范围内使用:

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

Because I wanted to test a component that wrapped react-select , mocking it with a regular <select> element wouldn't have worked.因为我想测试一个包装react-select的组件,所以用常规的<select>元素模拟它是行不通的。 So I ended up using the same approach that the package's own tests use, which is supplying a className in props and then using it with querySelector() to access the rendered element in the test:所以我最终使用了包自己的测试使用的相同方法,即在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');

An easy way to test is by doing what the user should do一个简单的测试方法是做用户应该做的事情

  • Click on the select field.单击 select 字段。
  • Click on one of the items in the dropdown list.单击下拉列表中的一项。
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();
});

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM