测试React Select组件

44

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

我想使用React-Select组件,但需要添加测试。
我尝试了一些在Google上找到的选项,但似乎都不起作用。我有下面的代码,但它没有触发更改事件。我已经能够添加一个焦点事件,它会添加“is-focussed”类,但是“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个回答

39

这是一个经常出现的问题。我分享了自己的代码,其中包含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个选项的select组件。

enter image description here

测试1:应该无错误地呈现

  • 我呈现组件。

  • 我搜索占位符是否存在。

测试2:当选择第一个选项时应调用onChange

  • 我呈现组件。

  • 我检查my mockedOnChange是否尚未被调用。

  • 模拟一个ArrowDown事件。

  • 单击第一个选项。

  • 我检查mockedOnChange是否被调用1次,且为第一个选项标签和值。

测试3:当选择第一个选项、然后第二个选项,最后第9个选项时应调用onChange

  • 我呈现组件。

  • 我模拟选择第一个选项。

  • 我模拟选择第二个选项。

  • 我模拟选择第9个选项。

  • 我检查mockedOnChange是否被调用3次,且为第9个选项的标签和值。

测试4:当通过输入值过滤时应调用onChange

  • 我呈现组件。

  • 我模拟更改输入字段,输入"option 1"。

  • 根据我的mockedOptions,我知道过滤结果将是"Mocked option 1"和"Mocked option 10"。

  • 我模拟2个ArrowDown事件。

  • 我检查mockedOnChange是否以正确的标签和值调用了第二个筛选选项。

完整的测试文件

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

});

我希望这能够帮到你。


3
谢谢您的回答,它在使用@testing-library/react和最新版本的react-select上非常有效。然而,对于任何来到这里的人,不建议使用模拟测试onChange方法(toHaveBeenCalled)。相反,您应该使用@testing-library/react中公开的getByTextqueryByText方法查找是否已选择选项进行测试。在我的情况下,我确保占位符被删除:expect(screen.queryByText('placeholder')).not.toBeInTheDocument()并被选项替换:expect(screen.getByText('opt')).toBeVisible(); - TotomInc

25

我已经尝试了上面列出的两个答案,但还是不行。

对我而言有效的方法是:

  1. 添加classNamePrefix属性 - 即list(如其他答案中所述):

    <Select
       classNamePrefix='list'
       options={[
         { label: 'one', value: 'one' },
         { label: 'two', value: 'two' }
    ]}/>
    
  2. 选择下拉指示器并模拟鼠标按下=>打开下拉菜单:

    wrapper
      .find('.list__dropdown-indicator')
      .simulate('mouseDown', {
        button: 0 
      });
    
    期望事情发生,例如在我的情况下,我正在检查下拉选项的数量。
  3. expect(wrapper.find('.list__option').length).toEqual(2);
    

    如果您可以控制发送的道具,您可以添加menuIsOpen道具始终保持菜单打开(也称为列表中的第二步)。

要从下拉菜单中选择值,请在打开下拉菜单后:

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

menuIsOpen 属性设置为 true 不容易,因为它会改变组件的行为。相反,您使用 classNamePrefix 的解决方案非常好用!谢谢! - LucioB
这个方法可以运行,但我必须将它更改为 expect(wrapper.update().find('.list__option').length).toEqual(2); 才能使其正确运行。 - fabian enos
可能是因为你检查的是.length而不是.text() @fabian-enos ?:),当然,我应该提供更好的文本示例。 - Sanda
啊不,我想这是因为我在选择前后检查了single-value的存在。我刚刚尝试了一些没有update()的地方,但似乎仍然需要它才能正确找到它。 - fabian enos

23

来自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')

2
重要提示是,这两个keyDown模拟都需要触发更改事件。 - Kyle Suss

13

使用testing-library和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'});

你的解决方案确实打开了菜单,但没有点击选项二(没有点击任何选项。它只是关闭了菜单)@dmitriy - GO VEGAN
2
对我来说,它非常好用,一切都很顺利(选项已正确点击)。然而,遗憾的是我们被迫通过键盘事件完成第一部分。这感觉就像我们只测试了a11y方面,这将是一个很好的附加测试案例,但不是主要的。不过,我想我们可以依赖react-select本身具有适当的测试覆盖率,并安全地使用这种方法/解决方法。 - loopmode

5

作为 Keith 所说的补充,使用 simulate 方法似乎是练习功能的唯一方法。 但是,当我在我的解决方案中尝试时,它不起作用 - 我正在使用 TypeScript,所以不确定这是否有影响,但我发现模拟事件时还需要传递 key 属性:

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

我还发现设置classNamePrefix属性使得进行简单测试更加容易,从而让我有信心组件能够正确响应模拟事件。当设置这个前缀时,有用的组件部分会被装饰上类名,提供了轻松访问它们的方式(你可以通过在谷歌开发工具中检查元素来识别这些有用的类名)。以下是一个简单的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');
});

4

对于较新的版本React-Select(2.x+),以上方法不起作用,因为React-Select使用了emotion。 因此,wrapper.find('.Select-control')wrapper.find('.list__option')不再起作用。 React-Select 2.x+将Select组件包装在状态管理器中,但您可以访问Select组件的onChange方法。 我使用以下代码来触发选择:

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

1
你如何定义这个测试中的 Select - Chris
1
@Chris 要么从 'react-select' 导入它,要么将它放在 'Select' 字符串中。 - Gopal Saini
1
如果你只是手动调用它的 onChange,那你在测试什么呢? - George Mauer
例如,如果您正在测试表单的验证,并且它显示错误。调用onChange的方法并不重要,因为这不是测试的重点。测试的重点是测试在输入无效值(即无效的电子邮件地址)时是否显示错误。 - Armin Primadi

2

我想补充一下,实际上我必须将classNamePrefix属性添加到Select组件中,否则我就无法获取任何*__control类来使用。


2

我也有同样的问题需要测试react-select组件,我的解决方案如下。

我的react-select组件:

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


2

如果有人使用enzyme库,这对我起作用:

最初的回答:

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

“select”是指在此处定义的属性名称:

最初的回答

  <Select
      name="select"
      ...

最初的回答: "value" 是所需选项的值。

1
在我的情况下,它使用 wrapper.find('Select').simulate('change', {value: 1, label: 'label' }); 工作。不需要将其包装在 target 中。 - Shubham Jain

2

有一个库可以模拟用户在react-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"],
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接