react-select-module
Version:
A Select control built with and for ReactJS
1,619 lines (1,531 loc) • 79.9 kB
JavaScript
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import cases from 'jest-in-case';
import {
OPTIONS,
OPTIONS_ACCENTED,
OPTIONS_NUMBER_VALUE,
OPTIONS_BOOLEAN_VALUE,
OPTIONS_DISABLED,
} from './constants';
import Select from '../Select';
import { matchers } from 'jest-emotion';
expect.extend(matchers);
const BASIC_PROPS = {
className: 'react-select',
classNamePrefix: 'react-select',
onChange: jest.fn(),
onInputChange: jest.fn(),
onMenuClose: jest.fn(),
onMenuOpen: jest.fn(),
name: 'test-input-name',
options: OPTIONS,
};
test('snapshot - defaults', () => {
const { container } = render(<Select />);
expect(container).toMatchSnapshot();
});
test('instanceId prop > to have instanceId as id prefix for the select components', () => {
let { container } = render(
<Select {...BASIC_PROPS} menuIsOpen instanceId={'custom-id'} />
);
expect(container.querySelector('input').id).toContain('custom-id');
container.querySelectorAll('div.react-select__option').forEach(opt => {
expect(opt.id).toContain('custom-id');
});
});
test('hidden input field is not present if name is not passes', () => {
let { container } = render(<Select options={OPTIONS} />);
expect(container.querySelector('input[type="hidden"]')).toBeNull();
});
test('hidden input field is present if name passes', () => {
let { container } = render(
<Select name="test-input-name" options={OPTIONS} />
);
expect(container.querySelector('input[type="hidden"]')).toBeTruthy();
});
test('single select > passing multiple values > should select the first value', () => {
const props = { ...BASIC_PROPS, value: [OPTIONS[0], OPTIONS[4]] };
let { container } = render(<Select {...props} />);
expect(container.querySelector('.react-select__control').textContent).toBe(
'0'
);
});
test('isRtl boolean prop sets direction: rtl on container', () => {
let { container } = render(
<Select {...BASIC_PROPS} value={[OPTIONS[0]]} isRtl isClearable />
);
expect(container.firstChild).toHaveStyleRule('direction', 'rtl');
});
test('isOptionSelected() prop > single select > mark value as isSelected if isOptionSelected returns true for the option', () => {
// Select all but option with label '1'
let isOptionSelected = jest.fn(option => option.label !== '1');
let { container } = render(
<Select {...BASIC_PROPS} isOptionSelected={isOptionSelected} menuIsOpen />
);
let options = container.querySelectorAll('.react-select__option');
// Option label 0 to be selected
expect(options[0].classList).toContain('react-select__option--is-selected');
// Option label 1 to be not selected
expect(options[1].classList).not.toContain(
'react-select__option--is-selected'
);
});
test('isOptionSelected() prop > multi select > to not show the selected options in Menu for multiSelect', () => {
// Select all but option with label '1'
let isOptionSelected = jest.fn(option => option.label !== '1');
let { container } = render(
<Select
{...BASIC_PROPS}
isMulti
isOptionSelected={isOptionSelected}
menuIsOpen
/>
);
expect(container.querySelectorAll('.react-select__option')).toHaveLength(1);
expect(container.querySelector('.react-select__option').textContent).toBe(
'1'
);
});
cases(
'formatOptionLabel',
({ props, valueComponentSelector, expectedOptions }) => {
let { container } = render(<Select {...props} />);
let value = container.querySelector(valueComponentSelector);
expect(value.textContent).toBe(expectedOptions);
},
{
'single select > should format label of options according to text returned by formatOptionLabel': {
props: {
...BASIC_PROPS,
formatOptionLabel: ({ label, value }, { context }) =>
`${label} ${value} ${context}`,
value: OPTIONS[0],
},
valueComponentSelector: '.react-select__single-value',
expectedOptions: '0 zero value',
},
'multi select > should format label of options according to text returned by formatOptionLabel': {
props: {
...BASIC_PROPS,
formatOptionLabel: ({ label, value }, { context }) =>
`${label} ${value} ${context}`,
isMulti: true,
value: OPTIONS[0],
},
valueComponentSelector: '.react-select__multi-value',
expectedOptions: '0 zero value',
},
}
);
cases(
'name prop',
({ expectedName, props }) => {
let { container } = render(<Select {...props} />);
let input = container.querySelector('input[type=hidden]');
expect(input.name).toBe(expectedName);
},
{
'single select > should assign the given name': {
props: { ...BASIC_PROPS, name: 'form-field-single-select' },
expectedName: 'form-field-single-select',
},
'multi select > should assign the given name': {
props: {
...BASIC_PROPS,
name: 'form-field-multi-select',
isMulti: true,
value: OPTIONS[2],
},
expectedName: 'form-field-multi-select',
},
}
);
cases(
'menuIsOpen prop',
({ props = BASIC_PROPS }) => {
let { container, rerender } = render(<Select {...props} />);
expect(container.querySelector('.react-select__menu')).toBeFalsy();
rerender(<Select {...props} menuIsOpen />);
expect(container.querySelector('.react-select__menu')).toBeTruthy();
rerender(<Select {...props} />);
expect(container.querySelector('.react-select__menu')).toBeFalsy();
},
{
'single select > should show menu if menuIsOpen is true and hide menu if menuIsOpen prop is false': {},
'multi select > should show menu if menuIsOpen is true and hide menu if menuIsOpen prop is false': {
props: {
...BASIC_PROPS,
isMulti: true,
},
},
}
);
cases(
'filterOption() prop - default filter behavior',
({ props, searchString, expectResultsLength }) => {
let { container, rerender } = render(<Select {...props} />);
rerender(<Select {...props} inputValue={searchString} />);
expect(container.querySelectorAll('.react-select__option')).toHaveLength(
expectResultsLength
);
},
{
'single select > should match accented char': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
options: OPTIONS_ACCENTED,
},
searchString: 'ecole', // should match "école"
expectResultsLength: 1,
},
'single select > should ignore accented char in query': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
options: OPTIONS_ACCENTED,
},
searchString: 'schoöl', // should match "school"
expectResultsLength: 1,
},
}
);
cases(
'filterOption() prop - should filter only if function returns truthy for value',
({ props, searchString, expectResultsLength }) => {
let { container, rerender } = render(<Select {...props} />);
rerender(<Select {...props} inputValue={searchString} />);
expect(container.querySelectorAll('.react-select__option')).toHaveLength(
expectResultsLength
);
},
{
'single select > should filter all options as per searchString': {
props: {
...BASIC_PROPS,
filterOption: (value, search) => value.value.indexOf(search) > -1,
menuIsOpen: true,
value: OPTIONS[0],
},
searchString: 'o',
expectResultsLength: 5,
},
'multi select > should filter all options other that options in value of select': {
props: {
...BASIC_PROPS,
filterOption: (value, search) => value.value.indexOf(search) > -1,
isMulti: true,
menuIsOpen: true,
value: OPTIONS[0],
},
searchString: 'o',
expectResultsLength: 4,
},
}
);
cases(
'filterOption prop is null',
({ props, searchString, expectResultsLength }) => {
let { container, rerender } = render(<Select {...props} />);
rerender(<Select {...props} inputValue={searchString} />);
expect(container.querySelectorAll('.react-select__option')).toHaveLength(
expectResultsLength
);
},
{
'single select > should show all the options': {
props: {
...BASIC_PROPS,
filterOption: null,
menuIsOpen: true,
value: OPTIONS[0],
},
searchString: 'o',
expectResultsLength: 17,
},
'multi select > should show all the options other than selected options': {
props: {
...BASIC_PROPS,
filterOption: null,
isMulti: true,
menuIsOpen: true,
value: OPTIONS[0],
},
searchString: 'o',
expectResultsLength: 16,
},
}
);
cases(
'no option found on search based on filterOption prop',
({ props, searchString }) => {
let { getByText, rerender } = render(<Select {...props} />);
rerender(<Select {...props} inputValue={searchString} />);
expect(getByText('No options').className).toContain(
'menu-notice--no-options'
);
},
{
'single Select > should show NoOptionsMessage': {
props: {
...BASIC_PROPS,
filterOption: (value, search) => value.value.indexOf(search) > -1,
menuIsOpen: true,
},
searchString: 'some text not in options',
},
'multi select > should show NoOptionsMessage': {
props: {
...BASIC_PROPS,
filterOption: (value, search) => value.value.indexOf(search) > -1,
menuIsOpen: true,
},
searchString: 'some text not in options',
},
}
);
cases(
'noOptionsMessage() function prop',
({ props, expectNoOptionsMessage, searchString }) => {
let { getByText, rerender } = render(<Select {...props} />);
rerender(<Select {...props} inputValue={searchString} />);
expect(getByText(expectNoOptionsMessage).className).toContain(
'menu-notice--no-options'
);
},
{
'single Select > should show NoOptionsMessage returned from noOptionsMessage function prop': {
props: {
...BASIC_PROPS,
filterOption: (value, search) => value.value.indexOf(search) > -1,
menuIsOpen: true,
noOptionsMessage: () =>
'this is custom no option message for single select',
},
expectNoOptionsMessage:
'this is custom no option message for single select',
searchString: 'some text not in options',
},
'multi select > should show NoOptionsMessage returned from noOptionsMessage function prop': {
props: {
...BASIC_PROPS,
filterOption: (value, search) => value.value.indexOf(search) > -1,
menuIsOpen: true,
noOptionsMessage: () =>
'this is custom no option message for multi select',
},
expectNoOptionsMessage:
'this is custom no option message for multi select',
searchString: 'some text not in options',
},
}
);
cases(
'value prop',
({ props, expectedValue }) => {
let value;
render(
<Select
{...props}
components={{
Control: ({ getValue }) => {
value = getValue();
return null;
},
}}
/>
);
expect(value).toEqual(expectedValue);
},
{
'single select > should set it as initial value': {
props: {
...BASIC_PROPS,
value: OPTIONS[2],
},
expectedValue: [{ label: '2', value: 'two' }],
},
'single select > with option values as number > should set it as initial value': {
props: {
...BASIC_PROPS,
value: OPTIONS_NUMBER_VALUE[2],
},
expectedValue: [{ label: '2', value: 2 }],
},
'multi select > should set it as initial value': {
props: {
...BASIC_PROPS,
isMulti: true,
value: OPTIONS[1],
},
expectedValue: [{ label: '1', value: 'one' }],
},
'multi select > with option values as number > should set it as initial value': {
props: {
...BASIC_PROPS,
isMulti: true,
value: OPTIONS_NUMBER_VALUE[1],
},
expectedValue: [{ label: '1', value: 1 }],
},
}
);
cases(
'update the value prop',
({
props = { ...BASIC_PROPS, value: OPTIONS[1] },
updateValueTo,
expectedInitialValue,
expectedUpdatedValue,
}) => {
let { container, rerender } = render(<Select {...props} />);
expect(container.querySelector('input[type="hidden"]').value).toEqual(
expectedInitialValue
);
rerender(<Select {...props} value={updateValueTo} />);
expect(container.querySelector('input[type="hidden"]').value).toEqual(
expectedUpdatedValue
);
},
{
'single select > should update the value when prop is updated': {
updateValueTo: OPTIONS[3],
expectedInitialValue: 'one',
expectedUpdatedValue: 'three',
},
'single select > value of options is number > should update the value when prop is updated': {
props: {
...BASIC_PROPS,
options: OPTIONS_NUMBER_VALUE,
value: OPTIONS_NUMBER_VALUE[2],
},
updateValueTo: OPTIONS_NUMBER_VALUE[3],
expectedInitialValue: '2',
expectedUpdatedValue: '3',
},
'multi select > should update the value when prop is updated': {
props: {
...BASIC_PROPS,
isMulti: true,
value: OPTIONS[1],
},
updateValueTo: OPTIONS[3],
expectedInitialValue: 'one',
expectedUpdatedValue: 'three',
},
'multi select > value of options is number > should update the value when prop is updated': {
props: {
...BASIC_PROPS,
delimiter: ',',
isMulti: true,
options: OPTIONS_NUMBER_VALUE,
value: OPTIONS_NUMBER_VALUE[2],
},
updateValueTo: [OPTIONS_NUMBER_VALUE[3], OPTIONS_NUMBER_VALUE[2]],
expectedInitialValue: '2',
expectedUpdatedValue: '3,2',
},
}
);
cases(
'calls onChange on selecting an option',
({
props = { ...BASIC_PROPS, menuIsOpen: true },
event: [eventName, eventOptions],
expectedSelectedOption,
optionsSelected,
focusedOption,
expectedActionMetaOption,
}) => {
let onChangeSpy = jest.fn();
props = { ...props, onChange: onChangeSpy };
let { container } = render(<Select {...props} />);
if (focusedOption) {
focusOption(container, focusedOption, props.options);
}
let selectOption = [
...container.querySelectorAll('div.react-select__option'),
].find(n => n.textContent === optionsSelected.label);
fireEvent[eventName](selectOption, eventOptions);
expect(onChangeSpy).toHaveBeenCalledWith(expectedSelectedOption, {
action: 'select-option',
option: expectedActionMetaOption,
name: BASIC_PROPS.name,
});
},
{
'single select > option is clicked > should call onChange() prop with selected option': {
event: ['click'],
optionsSelected: { label: '2', value: 'two' },
expectedSelectedOption: { label: '2', value: 'two' },
},
'single select > option with number value > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
options: OPTIONS_NUMBER_VALUE,
},
event: ['click'],
optionsSelected: { label: '0', value: 0 },
expectedSelectedOption: { label: '0', value: 0 },
},
'single select > option with boolean value > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
options: OPTIONS_BOOLEAN_VALUE,
},
event: ['click'],
optionsSelected: { label: 'true', value: true },
expectedSelectedOption: { label: 'true', value: true },
},
'single select > tab key is pressed while focusing option > should call onChange() prop with selected option': {
event: ['keyDown', { keyCode: 9, key: 'Tab' }],
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
expectedSelectedOption: { label: '1', value: 'one' },
},
'single select > enter key is pressed while focusing option > should call onChange() prop with selected option': {
event: ['keyDown', { keyCode: 13, key: 'Enter' }],
optionsSelected: { label: '3', value: 'three' },
focusedOption: { label: '3', value: 'three' },
expectedSelectedOption: { label: '3', value: 'three' },
},
'single select > space key is pressed while focusing option > should call onChange() prop with selected option': {
event: ['keyDown', { keyCode: 32, key: ' ' }],
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
expectedSelectedOption: { label: '1', value: 'one' },
},
'multi select > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
event: ['click'],
optionsSelected: { label: '2', value: 'two' },
expectedSelectedOption: [{ label: '2', value: 'two' }],
expectedActionMetaOption: { label: '2', value: 'two' },
},
'multi select > option with number value > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
options: OPTIONS_NUMBER_VALUE,
},
event: ['click'],
optionsSelected: { label: '0', value: 0 },
expectedSelectedOption: [{ label: '0', value: 0 }],
expectedActionMetaOption: { label: '0', value: 0 },
},
'multi select > option with boolean value > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
options: OPTIONS_BOOLEAN_VALUE,
},
event: ['click'],
optionsSelected: { label: 'true', value: true },
expectedSelectedOption: [{ label: 'true', value: true }],
expectedActionMetaOption: { label: 'true', value: true },
},
'multi select > tab key is pressed while focusing option > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
event: ['keyDown', { keyCode: 9, key: 'Tab' }],
menuIsOpen: true,
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
expectedSelectedOption: [{ label: '1', value: 'one' }],
expectedActionMetaOption: { label: '1', value: 'one' },
},
'multi select > enter key is pressed while focusing option > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
event: ['keyDown', { keyCode: 13, key: 'Enter' }],
optionsSelected: { label: '3', value: 'three' },
focusedOption: { label: '3', value: 'three' },
expectedSelectedOption: [{ label: '3', value: 'three' }],
expectedActionMetaOption: { label: '3', value: 'three' },
},
'multi select > space key is pressed while focusing option > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
event: ['keyDown', { keyCode: 32, key: ' ' }],
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
expectedSelectedOption: [{ label: '1', value: 'one' }],
expectedActionMetaOption: { label: '1', value: 'one' },
},
}
);
cases(
'calls onChange on de-selecting an option in multi select',
({
props = { ...BASIC_PROPS },
event: [eventName, eventOptions],
expectedSelectedOption,
expectedMetaOption,
optionsSelected,
focusedOption,
}) => {
let onChangeSpy = jest.fn();
props = {
...props,
onChange: onChangeSpy,
menuIsOpen: true,
hideSelectedOptions: false,
isMulti: true,
menuIsOpen: true,
};
let { container } = render(<Select {...props} />);
let selectOption = [
...container.querySelectorAll('div.react-select__option'),
].find(n => n.textContent === optionsSelected.label);
if (focusedOption) {
focusOption(container, focusedOption, props.options);
}
fireEvent[eventName](selectOption, eventOptions);
expect(onChangeSpy).toHaveBeenCalledWith(expectedSelectedOption, {
action: 'deselect-option',
option: expectedMetaOption,
name: BASIC_PROPS.name,
});
},
{
'option is clicked > should call onChange() prop with correct selected options and meta': {
props: {
...BASIC_PROPS,
options: OPTIONS,
value: [{ label: '2', value: 'two' }],
},
event: ['click'],
optionsSelected: { label: '2', value: 'two' },
expectedSelectedOption: [],
expectedMetaOption: { label: '2', value: 'two' },
},
'option with number value > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
options: OPTIONS_NUMBER_VALUE,
value: [{ label: '0', value: 0 }],
},
event: ['click'],
optionsSelected: { label: '0', value: 0 },
expectedSelectedOption: [],
expectedMetaOption: { label: '0', value: 0 },
},
'option with boolean value > option is clicked > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
options: OPTIONS_BOOLEAN_VALUE,
value: [{ label: 'true', value: true }],
},
event: ['click'],
optionsSelected: { label: 'true', value: true },
expectedSelectedOption: [],
expectedMetaOption: { label: 'true', value: true },
},
'tab key is pressed while focusing option > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
options: OPTIONS,
value: [{ label: '1', value: 'one' }],
},
event: ['keyDown', { keyCode: 9, key: 'Tab' }],
menuIsOpen: true,
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
expectedSelectedOption: [],
expectedMetaOption: { label: '1', value: 'one' },
},
'enter key is pressed while focusing option > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
options: OPTIONS,
value: { label: '3', value: 'three' },
},
event: ['keyDown', { keyCode: 13, key: 'Enter' }],
optionsSelected: { label: '3', value: 'three' },
focusedOption: { label: '3', value: 'three' },
expectedSelectedOption: [],
expectedMetaOption: { label: '3', value: 'three' },
},
'space key is pressed while focusing option > should call onChange() prop with selected option': {
props: {
...BASIC_PROPS,
options: OPTIONS,
value: [{ label: '1', value: 'one' }],
},
event: ['keyDown', { keyCode: 32, key: ' ' }],
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
expectedSelectedOption: [],
expectedMetaOption: { label: '1', value: 'one' },
},
}
);
function focusOption(container, option, options) {
let indexOfSelectedOption = options.findIndex(o => o.value === option.value);
for (let i = -1; i < indexOfSelectedOption; i++) {
fireEvent.keyDown(container.querySelector('.react-select__menu'), {
keyCode: 40,
key: 'ArrowDown',
});
}
expect(
container.querySelector('.react-select__option--is-focused').textContent
).toEqual(option.label);
}
cases(
'hitting escape on select option',
({
props,
event: [eventName, eventOptions],
focusedOption,
optionsSelected,
}) => {
let onChangeSpy = jest.fn();
let { container } = render(
<Select
{...props}
onChange={onChangeSpy}
onInputChange={jest.fn()}
onMenuClose={jest.fn()}
/>
);
let selectOption = [
...container.querySelectorAll('div.react-select__option'),
].find(n => n.textContent === optionsSelected.label);
focusOption(container, focusedOption, props.options);
fireEvent[eventName](selectOption, eventOptions);
expect(onChangeSpy).not.toHaveBeenCalled();
},
{
'single select > should not call onChange prop': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
},
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
event: ['keyDown', { keyCode: 27 }],
},
'multi select > should not call onChange prop': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
},
optionsSelected: { label: '1', value: 'one' },
focusedOption: { label: '1', value: 'one' },
event: ['keyDown', { keyCode: 27 }],
},
}
);
cases(
'click to open select',
({ props = BASIC_PROPS, expectedToFocus }) => {
let { container, rerender } = render(
<Select
{...props}
onMenuOpen={() => {
rerender(<Select {...props} menuIsOpen onMenuOpen={() => {}} />);
}}
/>
);
fireEvent.mouseDown(
container.querySelector('.react-select__dropdown-indicator'),
{ button: 0 }
);
expect(
container.querySelector('.react-select__option--is-focused').textContent
).toEqual(expectedToFocus.label);
},
{
'single select > should focus the first option': {
expectedToFocus: { label: '0', value: 'zero' },
},
'multi select > should focus the first option': {
props: {
...BASIC_PROPS,
isMulti: true,
},
expectedToFocus: { label: '0', value: 'zero' },
},
}
);
test('clicking when focused does not open select when openMenuOnClick=false', () => {
let spy = jest.fn();
let { container } = render(
<Select {...BASIC_PROPS} openMenuOnClick={false} onMenuOpen={spy} />
);
// this will get updated on input click, though click on input is not bubbling up to control component
userEvent.click(container.querySelector('.react-select__input input'));
expect(spy).not.toHaveBeenCalled();
});
cases(
'focus on options > keyboard interaction with Menu',
({ props, selectedOption, nextFocusOption, keyEvent = [] }) => {
let { container } = render(
<Select classNamePrefix="react-select" {...props} />
);
let indexOfSelectedOption = props.options.indexOf(selectedOption);
for (let i = -1; i < indexOfSelectedOption; i++) {
fireEvent.keyDown(container.querySelector('.react-select__menu'), {
keyCode: 40,
key: 'ArrowDown',
});
}
expect(
container.querySelector('.react-select__option--is-focused').textContent
).toEqual(selectedOption.label);
for (let event of keyEvent) {
fireEvent.keyDown(container.querySelector('.react-select__menu'), event);
}
expect(
container.querySelector('.react-select__option--is-focused').textContent
).toEqual(nextFocusOption.label);
},
{
'single select > ArrowDown key on first option should focus second option': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
},
keyEvent: [{ keyCode: 40, key: 'ArrowDown' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[1],
},
'single select > ArrowDown key on last option should focus first option': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 40, key: 'ArrowDown' }],
selectedOption: OPTIONS[OPTIONS.length - 1],
nextFocusOption: OPTIONS[0],
},
'single select > ArrowUp key on first option should focus last option': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 38, key: 'ArrowUp' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[OPTIONS.length - 1],
},
'single select > ArrowUp key on last option should focus second last option': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 38, key: 'ArrowUp' }],
selectedOption: OPTIONS[OPTIONS.length - 1],
nextFocusOption: OPTIONS[OPTIONS.length - 2],
},
'single select > disabled options should be focusable': {
props: {
menuIsOpen: true,
options: OPTIONS_DISABLED,
},
keyEvent: [{ keyCode: 40, key: 'ArrowDown' }],
selectedOption: OPTIONS_DISABLED[0],
nextFocusOption: OPTIONS_DISABLED[1],
},
'single select > PageDown key takes us to next page with default page size of 5': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 34, key: 'PageDown' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[5],
},
'single select > PageDown key takes us to next page with custom pageSize 7': {
props: {
menuIsOpen: true,
pageSize: 7,
options: OPTIONS,
},
keyEvent: [{ keyCode: 34, key: 'PageDown' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[7],
},
'single select > PageDown key takes to the last option is options below is less then page size': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 34, key: 'PageDown' }],
selectedOption: OPTIONS[OPTIONS.length - 3],
nextFocusOption: OPTIONS[OPTIONS.length - 1],
},
'single select > PageUp key takes us to previous page with default page size of 5': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 33, key: 'PageUp' }],
selectedOption: OPTIONS[6],
nextFocusOption: OPTIONS[1],
},
'single select > PageUp key takes us to previous page with custom pageSize of 7': {
props: {
menuIsOpen: true,
pageSize: 7,
options: OPTIONS,
},
keyEvent: [{ keyCode: 33, key: 'PageUp' }],
selectedOption: OPTIONS[9],
nextFocusOption: OPTIONS[2],
},
'single select > PageUp key takes us to first option - (previous options < pageSize)': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 33, key: 'PageUp' }],
selectedOption: OPTIONS[1],
nextFocusOption: OPTIONS[0],
},
'single select > Home key takes up to the first option': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 36, key: 'Home' }],
selectedOption: OPTIONS[OPTIONS.length - 3],
nextFocusOption: OPTIONS[0],
},
'single select > End key takes down to the last option': {
props: {
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 35, key: 'End' }],
selectedOption: OPTIONS[2],
nextFocusOption: OPTIONS[OPTIONS.length - 1],
},
'multi select > ArrowDown key on first option should focus second option': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 40, key: 'ArrowDown' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[1],
},
'multi select > ArrowDown key on last option should focus first option': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 40, key: 'ArrowDown' }],
selectedOption: OPTIONS[OPTIONS.length - 1],
nextFocusOption: OPTIONS[0],
},
'multi select > ArrowUp key on first option should focus last option': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 38, key: 'ArrowUp' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[OPTIONS.length - 1],
},
'multi select > ArrowUp key on last option should focus second last option': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 38, key: 'ArrowUp' }],
selectedOption: OPTIONS[OPTIONS.length - 1],
nextFocusOption: OPTIONS[OPTIONS.length - 2],
},
'multi select > PageDown key takes us to next page with default page size of 5': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 34, key: 'PageDown' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[5],
},
'multi select > PageDown key takes us to next page with custom pageSize of 8': {
props: {
isMulti: true,
menuIsOpen: true,
pageSize: 8,
options: OPTIONS,
},
keyEvent: [{ keyCode: 34, key: 'PageDown' }],
selectedOption: OPTIONS[0],
nextFocusOption: OPTIONS[8],
},
'multi select > PageDown key takes to the last option is options below is less then page size': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 34, key: 'PageDown' }],
selectedOption: OPTIONS[OPTIONS.length - 3],
nextFocusOption: OPTIONS[OPTIONS.length - 1],
},
'multi select > PageUp key takes us to previous page with default page size of 5': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 33, key: 'PageUp' }],
selectedOption: OPTIONS[6],
nextFocusOption: OPTIONS[1],
},
'multi select > PageUp key takes us to previous page with default page size of 9': {
props: {
isMulti: true,
menuIsOpen: true,
pageSize: 9,
options: OPTIONS,
},
keyEvent: [{ keyCode: 33, key: 'PageUp' }],
selectedOption: OPTIONS[10],
nextFocusOption: OPTIONS[1],
},
'multi select > PageUp key takes us to first option - previous options < pageSize': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 33, key: 'PageUp' }],
selectedOption: OPTIONS[1],
nextFocusOption: OPTIONS[0],
},
'multi select > Home key takes up to the first option': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 36, key: 'Home' }],
selectedOption: OPTIONS[OPTIONS.length - 3],
nextFocusOption: OPTIONS[0],
},
'multi select > End key takes down to the last option': {
props: {
isMulti: true,
menuIsOpen: true,
options: OPTIONS,
},
keyEvent: [{ keyCode: 35, key: 'End' }],
selectedOption: OPTIONS[2],
nextFocusOption: OPTIONS[OPTIONS.length - 1],
},
}
);
// TODO: Cover more scenario
cases(
'hitting escape with inputValue in select',
({ props }) => {
let spy = jest.fn();
let { container } = render(
<Select {...props} onInputChange={spy} onMenuClose={jest.fn()} />
);
fireEvent.keyDown(container.querySelector('.react-select'), {
keyCode: 27,
key: 'Escape',
});
expect(spy).toHaveBeenCalledWith('', { action: 'menu-close' });
},
{
'single select > should call onInputChange prop with empty string as inputValue': {
props: {
...BASIC_PROPS,
inputValue: 'test',
menuIsOpen: true,
value: OPTIONS[0],
},
},
'multi select > should call onInputChange prop with empty string as inputValue': {
props: {
...BASIC_PROPS,
inputValue: 'test',
isMulti: true,
menuIsOpen: true,
value: OPTIONS[0],
},
},
}
);
cases(
'Clicking dropdown indicator on select with closed menu with primary button on mouse',
({ props = BASIC_PROPS }) => {
let onMenuOpenSpy = jest.fn();
props = { ...props, onMenuOpen: onMenuOpenSpy };
let { container } = render(<Select {...props} />);
// Menu is closed
expect(
container.querySelector('.react-select__menu')
).not.toBeInTheDocument();
fireEvent.mouseDown(
container.querySelector('div.react-select__dropdown-indicator'),
{ button: 0 }
);
expect(onMenuOpenSpy).toHaveBeenCalled();
},
{
'single select > should call onMenuOpen prop when select is opened and onMenuClose prop when select is closed': {},
'multi select > should call onMenuOpen prop when select is opened and onMenuClose prop when select is closed': {
props: {
...BASIC_PROPS,
isMulti: true,
},
},
}
);
cases(
'Clicking dropdown indicator on select with open menu with primary button on mouse',
({ props = BASIC_PROPS }) => {
let onMenuCloseSpy = jest.fn();
props = { ...props, onMenuClose: onMenuCloseSpy };
let { container } = render(<Select {...props} menuIsOpen />);
// Menu is open
expect(container.querySelector('.react-select__menu')).toBeInTheDocument();
fireEvent.mouseDown(
container.querySelector('div.react-select__dropdown-indicator'),
{ button: 0 }
);
expect(onMenuCloseSpy).toHaveBeenCalled();
},
{
'single select > should call onMenuOpen prop when select is opened and onMenuClose prop when select is closed': {},
'multi select > should call onMenuOpen prop when select is opened and onMenuClose prop when select is closed': {
props: {
...BASIC_PROPS,
isMulti: true,
},
},
}
);
cases(
'Clicking Enter on a focused select',
({ props, expectedValue }) => {
let event;
let { container } = render(
<div
onKeyDown={_event => {
event = _event;
event.persist();
}}
>
<Select {...props} />
</div>
);
if (props.menuIsOpen) {
fireEvent.keyDown(container.querySelector('.react-select__menu'), {
keyCode: 40,
key: 'ArrowDown',
});
}
fireEvent.keyDown(container.querySelector('.react-select'), {
key: 'Enter',
keyCode: 13,
});
expect(event.defaultPrevented).toBe(expectedValue);
},
{
'while menuIsOpen && focusedOption && !isComposing > should invoke event.preventDefault': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
},
expectedValue: true,
},
'while !menuIsOpen > should not invoke event.preventDefault': {
props: {
...BASIC_PROPS,
},
expectedValue: false,
},
}
);
// QUESTION: Is this test right? I tried right clicking on the dropdown indicator in a browser and the select opened but this test says it shouldn't?
cases(
'clicking on select using secondary button on mouse',
({ props = BASIC_PROPS }) => {
let onMenuOpenSpy = jest.fn();
let onMenuCloseSpy = jest.fn();
let { container, rerender } = render(
<Select
{...props}
onMenuClose={onMenuCloseSpy}
onMenuOpen={onMenuOpenSpy}
/>
);
let downButton = container.querySelector(
'div.react-select__dropdown-indicator'
);
// does not open menu if menu is closed
fireEvent.mouseDown(downButton, { button: 1 });
expect(onMenuOpenSpy).not.toHaveBeenCalled();
// does not close menu if menu is opened
rerender(
<Select
{...props}
menuIsOpen
onMenuClose={onMenuCloseSpy}
onMenuOpen={onMenuOpenSpy}
/>
);
fireEvent.mouseDown(downButton, { button: 1 });
expect(onMenuCloseSpy).not.toHaveBeenCalled();
},
{
'single select > secondary click is ignored > should not call onMenuOpen and onMenuClose prop': {
skip: true,
},
'multi select > secondary click is ignored > should not call onMenuOpen and onMenuClose prop': {
props: {
...BASIC_PROPS,
isMulti: true,
},
skip: true,
},
}
);
cases(
'required on input is not there by default',
({ props = BASIC_PROPS }) => {
let { container } = render(<Select {...props} onInputChange={jest.fn()} />);
let input = container.querySelector('.react-select__input input');
expect(input.required).toBe(false);
},
{
'single select > should not have required attribute': {},
'multi select > should not have required attribute': { isMulti: true },
}
);
cases(
'value of hidden input control',
({ props, expectedValue }) => {
let { container } = render(<Select {...props} />);
let hiddenInput = container.querySelector('input[type="hidden"]');
expect(hiddenInput.value).toEqual(expectedValue);
},
{
'single select > should set value of input as value prop': {
props: {
...BASIC_PROPS,
value: OPTIONS[3],
},
expectedValue: 'three',
},
'single select > options with number values > should set value of input as value prop': {
props: {
...BASIC_PROPS,
options: OPTIONS_NUMBER_VALUE,
value: OPTIONS_NUMBER_VALUE[3],
},
expectedValue: '3',
},
'single select > options with boolean values > should set value of input as value prop': {
props: {
...BASIC_PROPS,
options: OPTIONS_BOOLEAN_VALUE,
value: OPTIONS_BOOLEAN_VALUE[1],
},
expectedValue: 'false',
},
'multi select > should set value of input as value prop': {
props: {
...BASIC_PROPS,
isMulti: true,
value: OPTIONS[3],
},
expectedValue: 'three',
},
'multi select > with delimiter prop > should set value of input as value prop': {
props: {
...BASIC_PROPS,
delimiter: ', ',
isMulti: true,
value: [OPTIONS[3], OPTIONS[5]],
},
expectedValue: 'three, five',
},
'multi select > options with number values > should set value of input as value prop': {
props: {
...BASIC_PROPS,
isMulti: true,
options: OPTIONS_NUMBER_VALUE,
value: OPTIONS_NUMBER_VALUE[3],
},
expectedValue: '3',
},
'multi select > with delimiter prop > options with number values > should set value of input as value prop': {
props: {
...BASIC_PROPS,
delimiter: ', ',
isMulti: true,
options: OPTIONS_NUMBER_VALUE,
value: [OPTIONS_NUMBER_VALUE[3], OPTIONS_NUMBER_VALUE[1]],
},
expectedValue: '3, 1',
},
'multi select > options with boolean values > should set value of input as value prop': {
props: {
...BASIC_PROPS,
isMulti: true,
options: OPTIONS_BOOLEAN_VALUE,
value: OPTIONS_BOOLEAN_VALUE[1],
},
expectedValue: 'false',
},
'multi select > with delimiter prop > options with boolean values > should set value of input as value prop': {
props: {
...BASIC_PROPS,
delimiter: ', ',
isMulti: true,
options: OPTIONS_BOOLEAN_VALUE,
value: [OPTIONS_BOOLEAN_VALUE[1], OPTIONS_BOOLEAN_VALUE[0]],
},
expectedValue: 'false, true',
},
}
);
cases(
'isOptionDisabled() prop',
({ props, expectedEnabledOption, expectedDisabledOption }) => {
let { container } = render(<Select {...props} />);
const enabledOptionsValues = [
...container.querySelectorAll('.react-select__option'),
]
.filter(n => !n.classList.contains('react-select__option--is-disabled'))
.map(option => option.textContent);
enabledOptionsValues.forEach(option => {
expect(expectedDisabledOption.indexOf(option)).toBe(-1);
});
const disabledOptionsValues = [
...container.querySelectorAll('.react-select__option'),
]
.filter(n => n.classList.contains('react-select__option--is-disabled'))
.map(option => option.textContent);
disabledOptionsValues.forEach(option => {
expect(expectedEnabledOption.indexOf(option)).toBe(-1);
});
},
{
'single select > should add isDisabled as true prop only to options that are disabled': {
props: {
...BASIC_PROPS,
menuIsOpen: true,
isOptionDisabled: option =>
['zero', 'two', 'five', 'ten'].indexOf(option.value) > -1,
},
expectedEnabledOption: ['1', '3', '11'],
expectedDisabledOption: ['0', '2', '5'],
},
'multi select > should add isDisabled as true prop only to options that are disabled': {
props: {
...BASIC_PROPS,
isMulti: true,
menuIsOpen: true,
isOptionDisabled: option =>
['zero', 'two', 'five', 'ten'].indexOf(option.value) > -1,
},
expectedEnabledOption: ['1', '3', '11'],
expectedDisabledOption: ['0', '2', '5'],
},
}
);
cases(
'isDisabled prop',
({ props }) => {
let { container } = render(<Select {...props} />);
let control = container.querySelector('.react-select__control');
expect(
control.classList.contains('react-select__control--is-disabled')
).toBeTruthy();
let input = container.querySelector('.react-select__control input');
expect(input.disabled).toBeTruthy();
},
{
'single select > should add isDisabled prop to select components': {
props: {
...BASIC_PROPS,
isDisabled: true,
},
},
'multi select > should add isDisabled prop to select components': {
props: {
...BASIC_PROPS,
isDisabled: true,
isMulti: true,
},
},
}
);
test('hitting Enter on option should not call onChange if the event comes from IME', () => {
let spy = jest.fn();
let { container } = render(
<Select
className="react-select"
classNamePrefix="react-select"
menuIsOpen
onChange={spy}
onInputChange={jest.fn()}
onMenuClose={jest.fn()}
options={OPTIONS}
tabSelectsValue={false}
/>
);
let selectOption = container.querySelector('div.react-select__option');
let menu = container.querySelector('.react-select__menu');
fireEvent.keyDown(menu, { keyCode: 40, key: 'ArrowDown' });
fireEvent.keyDown(menu, { keyCode: 40, key: 'ArrowDown' });
fireEvent.keyDown(selectOption, { keyCode: 229, key: 'Enter' });
expect(spy).not.toHaveBeenCalled();
});
test('hitting tab on option should not call onChange if tabSelectsValue is false', () => {
let spy = jest.fn();
let { container } = render(
<Select
className="react-select"
classNamePrefix="react-select"
menuIsOpen
onChange={spy}
onInputChange={jest.fn()}
onMenuClose={jest.fn()}
options={OPTIONS}
tabSelectsValue={false}
/>
);
let selectOption = container.querySelector('div.react-select__option');
let menu = container.querySelector('.react-select__menu');
fireEvent.keyDown(menu, { keyCode: 40, key: 'ArrowDown' });
fireEvent.keyDown(menu, { keyCode: 40, key: 'ArrowDown' });
fireEvent.keyDown(selectOption, { keyCode: 9, key: 'Tab' });
expect(spy).not.toHaveBeenCalled();
});
test('multi select > to not show selected value in options', () => {
let onInputChangeSpy = jest.fn();
let onMenuCloseSpy = jest.fn();
let { container, rerender } = render(
<Select
{...BASIC_PROPS}
isMulti
menuIsOpen
onInputChange={onInputChangeSpy}
onMenuClose={onMenuCloseSpy}
/>
);
let availableOptions = [
...container.querySelectorAll('.react-select__option'),
].map(option => option.textContent);
expect(availableOptions.indexOf('0') > -1).toBeTruthy();
rerender(
<Select
{...BASIC_PROPS}
isMulti
menuIsOpen
onInputChange={onInputChangeSpy}
onMenuClose={onMenuCloseSpy}
value={OPTIONS[0]}
/>
);
// Re-open Menu
fireEvent.mouseDown(
container.querySelector('div.react-select__dropdown-indicator'),
{
button: 0,
}
);
availableOptions = [
...container.querySelectorAll('.react-select__option'),
].map(option => option.textContent);
expect(availableOptions.indexOf('0') > -1).toBeFalsy();
});
test('multi select > to not hide the selected options from the menu if hideSelectedOptions is false', () => {
let { container } = render(
<Select
className="react-select"
classNamePrefix="react-select"
hideSelectedOptions={false}
isMulti
menuIsOpen
onChange={jest.fn()}
onInputChange={jest.fn()}
onMenuClose={jest.fn()}
options={OPTIONS}
/>
);
let firstOption = container.querySelectorAll('.react-select__option')[0];
let secondoption = container.querySelectorAll('.react-select__option')[1];
expect(firstOption.textContent).toBe('0');
expect(secondoption.textContent).toBe('1');
userEvent.click(firstOption);
expect(firstOption.textContent).toBe('0');
expect(secondoption.textContent).toBe('1');
});
test('multi select > call onChange with all values but last selected value and remove event on hitting backspace', () => {
let onChangeSpy = jest.fn();
let { container } = render(
<Select
{...BASIC_PROPS}
isMulti
onChange={onChangeSpy}
value={[OPTIONS[0], OPTIONS[1], OPTIONS[2]]}
/>
);
expect(container.querySelector('.react-select__control').textContent).toBe(
'012'
);
fireEvent.keyDown(container.querySelector('.react-select__control'), {
keyCode: 8,
key: 'Backspace',
});
expect(onChangeSpy).toHaveBeenCalledWith(
[
{ label: '0', value: 'zero' },
{ label: '1', value: 'one' },
],
{
action: 'pop-value',
removedValue: { label: '2', value: 'two' },
name: BASIC_PROPS.name,
}
);
});
test('should not call onChange on hitting backspace when backspaceRemovesValue is false', () => {
let onChangeSpy = jest.fn();
let { container } = render(
<Select
{...BASIC_PROPS}
backspaceRemovesValue={false}
onChange={onCh