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

how to test react-select with react-testing-library

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 回调?找到触发事件的元素的首选方法是什么?谢谢

这一定是关于 RTL 的最常见问题:D

最佳策略是使用 jest.mock(或测试框架中的等效项)来模拟 select 并呈现 HTML select。

有关为什么这是最佳方法的更多信息,我写了一些也适用于这种情况的内容。 OP 在 Material-UI 中询问了 select 但想法是一样的。

Original question 和我的回答:

Because you have no control over that UI. It's defined in a 3rd party module.

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. 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.

The other option is to trust that Material-UI is going to make a component that works and that your users can use. 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.

这是一个如何模拟 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>
  );
});

您可以阅读更多内容here

在我的项目中,我使用了 react-testing-library 和 jest-dom。 我 运行 遇到同样的问题 - 经过一番调查,我找到了解决方案,基于线程:https://github.com/airbnb/enzyme/issues/400

请注意,渲染的 top-level 函数以及各个步骤必须是异步的。

在这种情况下不需要使用焦点事件,它将允许 select 多个值。

此外,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');

  ( ... )

}

类似于@momimomo的回答,我写了一个小帮手来从TypeScript中的react-select中选择一个选项。

帮助文件:

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

注意: 容器:select 框的容器(例如:container = getByTestId('seclectTestId'))

最后,有一个库可以帮助我们解决这个问题:https://testing-library.com/docs/ecosystem-react-select-event。完美适用于单个 select 或 select-多个:

来自 @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 提供这么棒的软件包!

这个解决方案对我有用。

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

希望对奋斗者有所帮助

适用于我的用例并且不需要 react-select 模拟或单独库的替代解决方案(感谢 @Steve Vaughan) found on the react-testing-library spectrum chat.

这样做的缺点是我们必须使用 container.querySelector,RTL 建议不要使用它来支持其更具弹性的选择器。

如果您不使用 label 元素,使用 react-select-event 的方法是:

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

selectEvent.select(select, "Value");

如果出于某种原因存在同名标签,请使用此

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:

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

因为我想测试一个包装 react-select 的组件,所以用常规 <select> 元素模拟它是行不通的。所以我最终使用了 the package's own tests 使用的相同方法,即在 props 中提供 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');