【问题标题】:how to test react-select with react-testing-library如何使用 react-testing-library 测试 react-select
【发布时间】:2019-08-29 17:53:27
【问题描述】:

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

【问题讨论】:

    标签: reactjs integration-testing react-select react-testing-library


    【解决方案1】:

    这一定是关于 RTL 被问得最多的问题:D

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

    关于为什么这是最好的方法的更多信息,我写了一些适用于这种情况的东西。 OP询问了Material-UI中的选择,但想法是一样的。

    Original question 和我的回答:

    因为您无法控制该 UI。它在第 3 方模块中定义。

    所以,你有两个选择:

    您可以找出材质库创建的 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>
      );
    });
    

    您可以阅读更多here

    【讨论】:

    • @GiorgioPolvara-Gpx 虽然我得到了你建议的方法,但我很想知道这是否真的违反了测试库的指导原则。该库鼓励测试最终用户实际与之交互的内容(所以对我来说更多的是集成/功能测试而不是单元测试)。在您的方法中,您正在模拟外部依赖项(这对单元测试很有用),但是如果依赖项得到更新,那么就会对失败的软件进行成功的测试。您对此有何看法?
    • @GiorgioPolvara-Gpx 我读了你的博客,我使用的是react-select/async,所以我使用了jest.mock("react-select/async",...,但我得到一个无法通过以下方式找到元素:[data-testid= "select"] 在尝试fireEvent.change(getByTestId("select"), { target: { value: "foo" } }); 时,我有一个render(&lt;MySearchEngine /&gt;),这就像getByTestId 正在调查它而不是jest.mock 块。我错过了什么?谢谢
    • 如果他们将组件模拟到这种程度,人们应该对他们的组件的测试完全没有信心。我强烈建议不要采用这种方法。在这种情况下,您正在测试一个完全不同的组件。
    • 我很难从这里帮助你。在此处或官方 Spectrum 页面上打开一个新问题
    • @GiorgioPolvara-Gpx 我不同意你应该模拟第三方库。如果该库更改/中断,我想知道它(不必阅读更改日志/发行说明),测试是如何发生的。
    【解决方案2】:

    在我的项目中,我使用的是 react-testing-library 和 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');
    
      ( ... )
    
    }
    

    【讨论】:

    • 我个人更喜欢这个解决方案,而不是公认的答案,因为你保持原样。这样,您就可以真正像用户测试它们一样测试事物。如果你模拟react-select,你甚至需要测试你自己的模拟,这会适得其反。如果你使用更复杂的属性,react-select 提供你的模拟也会变得更复杂,也很难维护恕我直言
    • 这个答案效果很好,不需要模拟。谢谢!
    • 您是否已将其与 ant 4 一起使用?我有一个类似的解决方案,效果很好,但升级后找不到选项..
    • 虽然我不认为其他解决方案本质上是错误的,但我也更喜欢这种解决方案,因为它更接近现实世界的场景。感谢您分享此内容,这帮助我和我的同事解决了我们在模拟选择中遇到了一段时间但没有成功的问题。
    • 只是出色的解决方案。顺便说一句,现在不推荐使用 waitforElement() 。我这样做了:await screen.findByText(itemText);
    【解决方案3】:

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

    【讨论】:

    • 我最喜欢这个答案,不需要安装额外的包总是一个加分项
    【解决方案4】:
    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:选择框的容器(例如:container = getByTestId('seclectTestId'))

    【讨论】:

    • await wait() 是从哪里来的?
    • wait() 仅来自反应测试库。如果我们在 act() 中结合 fireEvent 会更好。
    • fireEvent 不需要包裹在 act() 中
    【解决方案5】:

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

    来自@testing-library/react docs:

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

    【讨论】:

    • 使用 Formik 和 chakra-ui 嵌入 react-select 就像一个魅力,前夕
    • 好东西 react-select-event,我一直在努力正确测试 react-select
    • 如果您想要开箱即用的东西,react-select 是一个很棒的包。不幸的是,可访问性和测试是痛苦的。还有,给项目带来了情感,不轻,一年前切换到downshift,永不回头。它需要一些设置,但结果更轻、更容易测试并且开箱即用。
    • @Constantin 我没有使用情感,只是普通的 CSS 模块来定制它
    【解决方案6】:

    这个解决方案对我有用。

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

    希望对奋斗者有所帮助。

    【讨论】:

    • react-select 不会将任何data-testid 传递给它的任何子元素,而且您不能自己提供它。您的解决方案适用于常规的 select HTML 元素,但恐怕它不适用于 react-select lib。
    • @StanleySathler 正确,这不适用于 react-select,而仅适用于 HTML select
    【解决方案7】:

    另一种解决方案适用于我的用例,不需要在 react-testing-library spectrum chat. 上找到 react-select 模拟或单独的库(感谢 @Steve Vaughan

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

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

        【解决方案9】:

        如果出于某种原因有一个同名的标签使用这个

        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——你可以使用 inside:

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

        【讨论】:

          猜你喜欢
          • 2019-05-21
          • 2021-11-30
          • 2019-11-11
          • 2021-02-11
          • 2022-08-04
          • 2021-02-28
          • 2019-10-30
          • 2023-04-04
          • 1970-01-01
          相关资源
          最近更新 更多