2

I'm attempting to test a Select input inside an Ant Design Form filled with initialValues and the test is failing because the Select does not receive a value. Is there a best way to test a "custom" rendered select?

Test Output:

Error: expect(element).toHaveValue(chocolate)

Expected the element to have value:
  chocolate
Received:

Example Test:

import { render, screen } from '@testing-library/react';
import { Form, Select } from 'antd';

const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
  wrapper: ({ children }) => children,
  ...options,
});

describe('select tests', () => {
  it('renders select', () => {
    const options = [
      { label: 'Chocolate', value: 'chocolate' },
      { label: 'Strawberry', value: 'strawberry' },
      { label: 'Vanilla', value: 'vanilla' },
    ];
    const { value } = options[0];

    customRender(
      <Form initialValues={{ formSelectItem: value }}>
        <Form.Item label="Form Select Label" name="formSelectItem">
          <Select options={options} />
        </Form.Item>
      </Form>,
    );

    expect(screen.getByLabelText('Form Select Label')).toHaveValue(value);
  });
});
proph3t
  • 865
  • 2
  • 7
  • 25

2 Answers2

1

I mocked a normal select and was able to get everything working.

The following example utilizes Vitest for a test runner but should apply similar to Jest.

antd-mock.tsx

import React from 'react';
import { vi } from 'vitest';

vi.mock('antd', async () => {
  const antd = await vi.importActual('antd');

  const Select = props => {
    const [text, setText] = React.useState('');
    const multiple = ['multiple', 'tags'].includes(props.mode);

    const handleOnChange = e => props.onChange(
      multiple
        ? Array.from(e.target.selectedOptions)
          .map(option => option.value)
        : e.target.value,
    );

    const handleKeyDown = e => {
      if (e.key === 'Enter') {
        props.onChange([text]);
        setText('');
      }
    };

    return (
      <>
        <select
          // add value in custom attribute to handle async selector,
          // where no option exists on load (need to type to fetch option)
          className={props.className}
          data-testid={props['data-testid']}
          data-value={props.value || undefined}
          defaultValue={props.defaultValue || undefined}
          disabled={props.disabled || undefined}
          id={props.id || undefined}
          multiple={multiple || undefined}
          onChange={handleOnChange}
          value={props.value || undefined}
        >
          {props.children}
        </select>
        {props.mode === 'tags' && (
          <input
            data-testid={`${props['data-testid']}Input`}
            onChange={e => setText(e.target.value)}
            onKeyDown={handleKeyDown}
            type="text"
            value={text}
          />
        )}
      </>
    );
  };

  Select.Option = ({ children, ...otherProps }) => (
    <option {...otherProps}>{children}</option>
  );
  Select.OptGroup = ({ children, ...otherProps }) => (
    <optgroup {...otherProps}>{children}</optgroup>
  );

  return { ...antd, Select };
});

utils.tsx

import { render } from '@testing-library/react';
import { ConfigProvider } from 'antd';

const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
  wrapper: ({ children }) => <ConfigProvider prefixCls="bingo">{children}</ConfigProvider>,
  ...options,
});

export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
export { customRender as render };

Select.test.tsx

import { Form } from 'antd';
import { render, screen, userEvent } from '../../../test/utils';
import Select from './Select';

const options = [
  { label: 'Chocolate', value: 'chocolate' },
  { label: 'Strawberry', value: 'strawberry' },
  { label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
const initialValues = { selectFormItem: value };

const renderSelect = () => render(
  <Form initialValues={initialValues}>
    <Form.Item label="Label" name="selectFormItem">
      <Select options={options} />
    </Form.Item>
  </Form>,
);

describe('select tests', () => {
  it('renders select', () => {
    render(<Select options={options} />);
    expect(screen.getByRole('combobox')).toBeInTheDocument();
  });

  it('renders select with initial values', () => {
    renderSelect();
    expect(screen.getByLabelText('Label')).toHaveValue(value);
  });

  it('handles select change', () => {
    renderSelect();
    expect(screen.getByLabelText('Label')).toHaveValue(value);
    userEvent.selectOptions(screen.getByLabelText('Label'), 'vanilla');
    expect(screen.getByLabelText('Label')).toHaveValue('vanilla');
  });
});
proph3t
  • 865
  • 2
  • 7
  • 25
0

testing a library component may be harsh sometimes because it hides internal complexity. for testing antd select i suggest to mock it and use normal select in your tests like this:

jest.mock('antd', () => {
        const antd = jest.requireActual('antd');

        const Select = ({ children, onChange, ...rest }) => {
            return <select role='combobox' onChange={e => onChange(e.target.value)}>
                    {children}
                </select>;
        };

        Select.Option = ({ children, ...otherProps }) => {
            return <option role='option' {...otherProps}}>{children}</option>;
        }

        return {
            ...antd,
            Select,
        }
    })

this way you can test the select component as a normal select (use screen.debug to check that the antd select is mocked)

free bug coding
  • 224
  • 2
  • 7
  • This works great for mocking the select but doesn't actually pass the form's `initialValues` props and still fails assertion. – proph3t Mar 24 '22 at 15:46