Skip to content

Instantly share code, notes, and snippets.

@asmyshlyaev177
Last active October 17, 2024 20:55
Show Gist options
  • Save asmyshlyaev177/4d99427fe58cb1a1fb24fc119fbe1400 to your computer and use it in GitHub Desktop.
Save asmyshlyaev177/4d99427fe58cb1a1fb24fc119fbe1400 to your computer and use it in GitHub Desktop.
Custom MaterialUI style Select written with TDD.
/* eslint-disable cypress/no-unnecessary-waiting */
import React from 'react';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { Popper, Paper } from '@material-ui/core';
import { styled } from '@material-ui/core/styles';
import { theme } from 'components/theme';
import {
CustomSelect,
omitCustomProps,
CustomNumberInput,
} from 'roles/member/pages/WhatsMyCopaySearch/components/Select';
const options = [
{ label: '100mi', value: '100' },
{ label: '200mi', value: '200' },
{ label: '300mi', value: '300' },
{ label: '456mi', value: '456' },
];
const SELECTORS = {
wrapper: '[data-testid="Select"]',
textField: '[data-testid="CustomSelect-text-field"]',
textFieldInput: `[data-testid="CustomSelect-text-field"] input`,
paper: '[data-testid="CustomSelect-paper"]',
listItem: '.option',
options: '[data-testid="CustomSelect-options-wrapper"]',
otherInput: '[data-testid="test"] input',
otherElement: '[data-testid="other"]',
customInputWrapper: '[data-testid="customInput-wrapper"]',
customInput: '[data-testid="customInput-wrapper"] input',
plusIconSelector: '[data-testid="plus-icon"]',
minusIconSelector: '[data-testid="minus-icon"]',
noOptions: '[data-testid="CustomSelect-no-options"]',
listbox: '[data-testid="CustomSelect-options-wrapper"]',
toggle: '[data-testid="CustomSelect-toggle"]',
close: '[data-testid="CustomSelect-close"]',
};
describe('CustomSelect', () => {
it('should display passed value', () => {
const value = options[0];
cy.mount(<Wrapper value={value} />);
assertInputValue(value.label);
});
it('should display selected value', () => {
const value = options[0];
cy.mount(<Wrapper value={value} options={options} />);
// open
openDropdown();
assertDropdownOpen().within(() => {
// selected and focused
cy.get(SELECTORS.listItem)
.filter('[aria-selected="true"]')
.filter('[data-focus="true"]')
.filter('.selected')
.should('have.length', 1)
.contains(value.label);
});
});
describe('disabled', () => {
it('input should be disabled', () => {
const value = options[0];
cy.mount(<Wrapper disabled={true} value={value} options={options} />);
cy.get(SELECTORS.textFieldInput).should('be.disabled');
});
it('options list should be disabled', () => {
const value = options[0];
cy.mount(<Wrapper disabled={true} value={value} options={options} />);
// eslint-disable-next-line cypress/no-force
cy.get(SELECTORS.textFieldInput).click({
force: true,
});
assertDropdownClosed();
});
it('icons should not react', () => {
const value = options[0];
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
onChange={onChangeSpy}
disabled={true}
value={value}
options={options}
showClearIcon={true}
/>
);
assertDropdownClosed();
// eslint-disable-next-line cypress/no-force
cy.get(SELECTORS.toggle).click({ force: true });
assertDropdownClosed();
// eslint-disable-next-line cypress/no-force
cy.get(SELECTORS.close).click({ force: true });
assertDropdownClosed();
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('should close', () => {
it('on click outside the list', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper onChange={onChangeSpy} options={options} />);
assertDropdownClosed();
// open
openDropdown();
assertDropdownOpen();
// click inside the list
// eslint-disable-next-line cypress/no-force
cy.get(SELECTORS.options).click({
force: true,
});
assertDropdownOpen();
// close
closeDropdown();
cy.wait(100);
assertDropdownClosed();
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('by Esc', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
openDropdown();
assertDropdownOpen();
closeDropdownByEsc();
cy.wait(100);
assertDropdownClosed();
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('when lose focus', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
openDropdown();
assertDropdownOpen();
cy.get(SELECTORS.otherInput).focus();
cy.wait(100);
assertDropdownClosed();
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('should open', () => {
it('on click', () => {
cy.mount(<Wrapper options={options} />);
assertDropdownClosed();
// open
openDropdown();
assertDropdownOpen();
});
it('when typing', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
const value = String(options[0].value).slice(0, 1);
openDropdown();
assertDropdownOpen();
closeDropdownByEsc();
assertDropdownClosed();
clearInput();
type(value);
assertInputValue(value);
assertDropdownOpen();
cy.get('@onChangeSpy').should('not.have.been.called');
});
describe('on focus', () => {
it('open by default', () => {
cy.mount(<Wrapper options={options} />);
assertDropdownClosed();
cy.get(SELECTORS.textFieldInput).focus();
assertDropdownOpen();
});
it('when openOnFocus=false should not open', () => {
cy.mount(<Wrapper openOnFocus={false} options={options} />);
assertDropdownClosed();
cy.get(SELECTORS.textFieldInput).focus();
assertDropdownClosed();
});
});
});
describe('Options list', () => {
describe('should display all options', () => {
it('when have selected value', () => {
const value = options[0];
cy.mount(<Wrapper value={value} options={options} />);
// open
openDropdown();
assertDropdownOpen().within(() => {
assertListLength(options.length);
});
});
it('when do not have selected value', () => {
cy.mount(<Wrapper options={options} />);
// open
openDropdown();
assertDropdownOpen().within(() => {
assertListLength(options.length);
cy.get(SELECTORS.listItem)
.filter('[aria-selected="true"]')
.should('have.length', 0);
});
});
});
describe('headerTitle', () => {
it('should display when passed', () => {
cy.mount(<Wrapper options={options} headerTitle="Title123" />);
openDropdown();
assertDropdownOpen().within(() => {
cy.contains('Title123');
});
});
it('should not display when empty', () => {
cy.mount(<Wrapper options={options} />);
openDropdown();
assertDropdownOpen().within(() => {
cy.get('[data-testid="CustomSelect-title"]').should('not.exist');
});
});
});
it('should toggle open/close by icon', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
toggeDropdown();
assertDropdownOpen();
toggeDropdown();
assertDropdownClosed();
toggeDropdown();
assertDropdownOpen();
toggeDropdown();
assertDropdownClosed();
cy.get('@onChangeSpy').should('not.have.been.called');
});
describe('should scroll to selected item', () => {
it('first item', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const longOptions = Array(50)
.fill(0)
.map((_el, ind) => ({
id: ind,
value: ind,
label: ind,
}));
const selected = longOptions[0];
cy.mount(
<Wrapper
value={selected}
options={longOptions}
onChange={onChangeSpy}
/>
);
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listbox).then($el => {
cy.wrap($el)
.invoke('scrollTop')
.should('be.closeTo', 1, 13);
});
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('middle item', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const longOptions = Array(50)
.fill(0)
.map((_el, ind) => ({
id: ind,
value: ind,
label: ind,
}));
const selected = longOptions[24];
cy.mount(
<Wrapper
value={selected}
options={longOptions}
onChange={onChangeSpy}
/>
);
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listbox).then($el => {
cy.wrap($el)
.invoke('scrollTop')
.should('be.closeTo', 1160, 1166);
});
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('last item', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const longOptions = Array(50)
.fill(0)
.map((_el, ind) => ({
id: ind,
value: ind,
label: ind,
}));
const selected = longOptions.slice(-1)[0];
cy.mount(
<Wrapper
value={selected}
options={longOptions}
onChange={onChangeSpy}
/>
);
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listbox).then($el => {
cy.wrap($el)
.invoke('scrollTop')
.should('be.closeTo', 2188, 2195);
});
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('long lists of options', () => {
it('should scroll to focused item', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const longOptions = Array(20)
.fill(0)
.map((_el, ind) => ({
id: ind,
value: ind,
label: ind,
}));
const selected = longOptions.slice(-1)[0];
cy.mount(
<Wrapper
value={selected}
options={longOptions}
onChange={onChangeSpy}
/>
);
openDropdown();
let scrollPos = 0;
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listbox).then($el => {
cy.wrap($el)
.invoke('scrollTop')
.then(scrollOffset => {
scrollPos = scrollOffset;
return scrollOffset;
})
.should('be.greaterThan', 100);
});
});
type('{upArrow}{upArrow}{upArrow}{upArrow}{upArrow}{upArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listbox).then($el => {
cy.wrap($el)
.invoke('scrollTop')
.should('lte', scrollPos);
});
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should be possible scroll to first/last options', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const longOptions = Array(30)
.fill(0)
.map((_el, ind) => ({
id: ind,
value: ind,
label: `_${ind}abc`,
}));
const selected = longOptions[14];
cy.mount(
<Wrapper
value={selected}
options={longOptions}
onChange={onChangeSpy}
/>
);
openDropdown();
const firstItem = `${SELECTORS.listItem}:contains("_0abc")`;
const lastItem = `${SELECTORS.listItem}:contains("_29abc")`;
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listbox).scrollTo('top');
cy.get(firstItem).should('be.visible');
cy.get(SELECTORS.listbox).scrollTo('bottom');
cy.get(lastItem).should('be.visible');
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('Select option', () => {
it('by click', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
const value = options[0];
const value2 = options[1];
// open
openDropdown();
assertDropdownOpen().within(() => {
cy.contains(value.label).click();
});
cy.get('@onChangeSpy').should('have.been.calledWith', value);
assertInputValue(value.label);
assertDropdownClosed();
// second time
openDropdown();
assertDropdownOpen().within(() => {
cy.contains(value2.label).click();
});
cy.get('@onChangeSpy').should('have.been.calledWith', value2);
assertInputValue(value2.label);
assertDropdownClosed();
});
});
describe('no options stub', () => {
describe('hideNoOptions', () => {
it('true by default', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
clearInput();
type('111');
cy.get(SELECTORS.noOptions).should('not.exist');
clearInput();
type('1');
cy.get(SELECTORS.noOptions).should('not.exist');
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('false - should display', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
hideNoOptions={false}
/>
);
assertDropdownClosed();
clearInput();
type('111');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.noOptions).should('be.visible');
});
clearInput();
type('1');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.noOptions).should('not.exist');
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
});
describe('filter', () => {
describe('default filter by opt.label', () => {
const optionsWithDifferentLabelValue = options.map(opt => ({
...opt,
label: +opt.value * 2 + 'mi',
}));
it('by partial match', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={optionsWithDifferentLabelValue}
onChange={onChangeSpy}
/>
);
assertDropdownClosed();
const value = optionsWithDifferentLabelValue[0];
clearInput();
type(value.label.slice(0, 2));
assertInputValue(value.label.slice(0, 2));
assertDropdownOpen().within(() => {
assertListLength(1).contains(value.label);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('by full match', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={optionsWithDifferentLabelValue}
onChange={onChangeSpy}
/>
);
assertDropdownClosed();
const value = optionsWithDifferentLabelValue[0];
const value2 = optionsWithDifferentLabelValue[1];
// first value
clearInput();
type(value.label);
assertInputValue(value.label);
assertDropdownOpen().within(() => {
assertListLength(1).contains(value.label);
});
clearInput();
assertDropdownOpen().within(() => {
assertListLength(options.length);
});
// second value
clearInput();
type(value2.label);
assertInputValue(value2.label);
assertDropdownOpen().within(() => {
assertListLength(1).contains(value2.label);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('when there are no options', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
hideNoOptions={false}
/>
);
assertDropdownClosed();
clearInput();
type('111');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.noOptions).should('be.visible');
});
clearInput();
type('1');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.noOptions).should('not.exist');
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('filterOptions prop', () => {
it('by partial match', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const filterOptions = ({ opt }) => !opt?.label?.includes('456');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
filterOptions={filterOptions}
/>
);
assertDropdownClosed();
const value = options[0];
clearInput();
type(value.label.slice(0, 2));
assertInputValue(value.label.slice(0, 2));
assertDropdownOpen().within(() => {
assertListLength(3);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('without filter', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const filterOptions = ({ opt }) => opt;
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
filterOptions={filterOptions}
/>
);
assertDropdownClosed();
const value = options[0];
clearInput();
type(value.label.slice(0, 2));
assertInputValue(value.label.slice(0, 2));
assertDropdownOpen().within(() => {
assertListLength(4);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('fuzzy search', () => {
describe('numbers', () => {
it('should fuzzy filter to closest number', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
fuzzy="numbers"
/>
);
assertDropdownClosed();
const value = options[0];
clearInput();
type(+value.value + 3);
assertDropdownOpen().within(() => {
assertListLength(1).contains(value.label);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('no options stub', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
fuzzy="numbers"
hideNoOptions={false}
/>
);
assertDropdownClosed();
clearInput();
type('abcdef');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.noOptions).should('be.visible');
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
});
});
describe('reset text to selected option', () => {
it('on close', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
const value = options[0];
// open
openDropdown();
assertDropdownOpen().within(() => {
cy.contains(value.label).click();
});
cy.get('@onChangeSpy').should('have.been.calledWith', value);
assertInputValue(value.label);
assertDropdownClosed();
// edit text and close
type('{backspace}');
cy.get(SELECTORS.otherInput).click();
assertDropdownClosed();
assertInputValue(value.label);
cy.get('@onChangeSpy').should('have.been.calledOnce');
// edit text and close by esc
type('{backspace}{esc}');
assertDropdownClosed();
assertInputValue(value.label);
cy.get('@onChangeSpy').should('have.been.calledOnce');
});
});
describe('keyboard navigation', () => {
it('no focused items on open', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.should('have.length', 0);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('selected item should be focused', () => {
const value = options[1];
cy.mount(<Wrapper options={options} value={value} />);
openDropdown();
assertDropdownOpen().within(() => {
assertListLength(options.length)
.filter('[data-focus="true"]')
.should('have.length', 1)
.contains(value.label);
});
});
it('should focus item by arrow up/down', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
openDropdown();
assertDropdownOpen();
// select down
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[0].value);
});
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[1].value);
});
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[2].value);
});
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[3].value);
});
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[3].value);
});
type('{upArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[2].value);
});
type('{upArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[1].value);
});
// select up
type('{upArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[0].value);
});
type('{upArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[0].value);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should reset focused after close dropdown', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
openDropdown();
// select down
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[0].value);
});
type('{downArrow}');
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.contains(options[1].value);
});
closeDropdown();
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.should('have.length', 0);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should select item by enter', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Wrapper options={options} onChange={onChangeSpy} />);
assertDropdownClosed();
openDropdown();
assertDropdownOpen();
// first item
type('{downArrow}{enter}');
cy.get('@onChangeSpy').should('have.been.calledWith', options[0]);
assertDropdownClosed();
// second item
openDropdown();
type('{downArrow}{enter}');
cy.get('@onChangeSpy').should('have.been.calledWith', options[1]);
assertDropdownClosed();
});
});
// For DrugSelect on Drug-table
describe('Custom onKeyDown handler', () => {
const onKeyDown = (_ev, { key, focused, setFocused }) => {
if (key === 'ArrowUp' || key === 'ArrowDown') {
return false;
}
if (key === 'Enter' && !!focused) {
return false;
}
if (key === 'Enter') {
return true;
}
if (key === 'Escape') {
return false;
}
setFocused(null);
};
it('should skip default ev handler if custom onKeyDown returns positive result', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
onKeyDown={onKeyDown}
/>
);
assertDropdownClosed();
const value = options[1];
type(`${value?.label.slice(0, 3)}{enter}`);
assertDropdownOpen().within(() => {
assertListLength(1).contains(value.label);
assertListLength(1)
.filter('[data-focus="true"]')
.should('have.length', 0);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('Arrow Down and select by enter', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
onKeyDown={onKeyDown}
/>
);
assertDropdownClosed();
const value = options[0];
type(`00{downArrow}`);
assertDropdownOpen().within(() => {
assertListLength(3)
.filter('[data-focus="true"]')
.contains(value.label);
});
cy.get('@onChangeSpy').should('not.have.been.called');
type(`{downArrow}{downArrow}{upArrow}{enter}`);
cy.get('@onChangeSpy').should('have.been.calledOnceWith', options[1]);
});
it('Arrow Up and select by enter', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
onKeyDown={onKeyDown}
/>
);
assertDropdownClosed();
const value = options[0];
type(`00{upArrow}`);
assertDropdownOpen().within(() => {
assertListLength(3)
.filter('[data-focus="true"]')
.contains(value.label);
});
cy.get('@onChangeSpy').should('not.have.been.called');
type(`{downArrow}{downArrow}{upArrow}{enter}`);
cy.get('@onChangeSpy').should('have.been.calledOnceWith', options[1]);
});
it('Should close by Esc', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
options={options}
onChange={onChangeSpy}
onKeyDown={onKeyDown}
/>
);
assertDropdownClosed();
type(`00{upArrow}{esc}`);
cy.get('@onChangeSpy').should('not.have.been.called');
assertDropdownClosed();
});
});
describe('custom component as listItem', () => {
const customOption = {
label: '',
value: '',
Component: CustomNumberInput,
custom: true,
id: 'custom1',
headerTitle: 'Select a custom value',
};
const optionsWithCustom = options.slice(0, 1).concat(customOption);
it('should render', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
assertDropdownOpen().within(() => {
assertListLength(optionsWithCustom.length);
assertCustomComponentVisible();
assertCustomComponentValue('0');
cy.get('[data-testid="customInput-title"]')
.should('be.visible')
.should('contain.text', customOption.headerTitle);
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should display custom value', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = {
...optionsWithCustom[1],
value: '555',
};
cy.mount(
<Wrapper
onChange={onChangeSpy}
value={value}
options={optionsWithCustom}
/>
);
openDropdown();
assertDropdownOpen().within(() => {
assertListLength(optionsWithCustom.length);
cy.get(SELECTORS.listItem)
.filter('[aria-selected="true"]')
.should('have.length', 1);
assertCustomComponentVisible();
assertCustomComponentValue('555');
});
closeDropdown();
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should update value on click outside', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = '123';
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
typeCustom(value);
closeDropdown();
assertInputValue(value);
cy.get('@onChangeSpy').should('have.been.calledWith', {
...omitCustomProps(optionsWithCustom[1]),
label: value,
value: value,
});
assertDropdownClosed();
});
it.skip('should not update value when dropdown still open(click outside CustomInput but inside dropdown)', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
onChange={onChangeSpy}
options={optionsWithCustom}
fuzzy="numbers"
/>
);
openDropdown();
assertDropdownOpen().within(() => {
assertCustomComponentVisible().within(() => {
cy.get(SELECTORS.plusIconSelector).click();
cy.get(SELECTORS.plusIconSelector).click();
});
});
// TODO: Can't click precisely between items :(
cy.get(SELECTORS.listbox).click(30, 1);
assertDropdownOpen();
assertCustomComponentVisible();
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should format label by labelFormatter', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = '123';
const labelFormatter = value => `${value} units`;
const optionWithFormatter = {
...customOption,
labelFormatter,
};
const optionsCustom = options.slice(0, 1).concat(optionWithFormatter);
cy.mount(<Wrapper onChange={onChangeSpy} options={optionsCustom} />);
openDropdown();
typeCustom(value);
closeDropdown();
assertInputValue(labelFormatter(value));
cy.get('@onChangeSpy').should('have.been.calledWith', {
...omitCustomProps(optionsWithCustom[1]),
label: labelFormatter(value),
value: value,
});
assertDropdownClosed();
});
it('should update value on click plus/minus icons', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
assertDropdownOpen().within(() => {
assertCustomComponentVisible().within(() => {
cy.get(SELECTORS.plusIconSelector).click();
cy.get(SELECTORS.plusIconSelector).click();
});
assertCustomComponentValue('2');
assertCustomComponentVisible().within(() => {
cy.get(SELECTORS.minusIconSelector).click();
cy.get(SELECTORS.minusIconSelector).click();
});
assertCustomComponentValue('0');
assertCustomComponentVisible().within(() => {
cy.get(SELECTORS.plusIconSelector).click();
cy.get(SELECTORS.plusIconSelector).click();
cy.get(SELECTORS.plusIconSelector).click();
});
});
closeDropdown();
cy.get('@onChangeSpy').should('have.been.calledWith', {
...omitCustomProps(optionsWithCustom[1]),
label: '3',
value: '3',
});
assertDropdownClosed();
});
it('should return focus to select after entering custom value', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = '123';
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
assertDropdownOpen().within(() => {
typeCustom(`${value}{enter}`);
});
assertDropdownClosed();
cy.get(SELECTORS.textFieldInput).should('be.focused');
});
describe('keyboard navigation', () => {
it('should focus on input', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
type('{downArrow}{downArrow}');
assertDropdownOpen().within(() => {
assertCustomComponentFocused();
});
cy.get(SELECTORS.customInputWrapper).type('{upArrow}');
assertDropdownOpen().within(() => {
assertCustomComponentNotFocused();
});
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('should update value on enter', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = '123';
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
cy.wait(100);
type('{downArrow}{enter}');
cy.get('@onChangeSpy').should(
'have.been.calledWith',
optionsWithCustom[0]
);
openDropdown();
cy.wait(100);
type(`{downArrow}`);
assertDropdownOpen().within(() => {
assertCustomComponentFocused().type(`${value}{enter}`);
});
assertInputValue(value);
cy.get('@onChangeSpy').should('have.been.calledWith', {
...omitCustomProps(optionsWithCustom[1]),
label: value,
value: value,
});
assertDropdownClosed();
});
it('should update value after loose/gain focus', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = '123';
cy.mount(
<Wrapper onChange={onChangeSpy} options={optionsWithCustom} />
);
openDropdown();
type(`{downArrow}{downArrow}`);
assertDropdownOpen().within(() => {
assertCustomComponentFocused().type(`${value}{upArrow}`);
});
type(`{downArrow}{enter}`);
assertInputValue(value);
cy.get('@onChangeSpy').should('have.been.calledWith', {
...omitCustomProps(optionsWithCustom[1]),
label: value,
value: value,
});
assertDropdownClosed();
});
it('custom item should be focused when it selected', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const customValue = '777';
cy.mount(
<Wrapper options={optionsWithCustom} onChange={onChangeSpy} />
);
// Select custom
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.customInput).clear();
cy.get(SELECTORS.customInput).type(customValue);
});
// blur
closeDropdown();
assertDropdownClosed();
// open again
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[aria-selected="true"]')
.eq(0)
.within(() => {
cy.get(`input`)
.invoke('val')
.should('eq', customValue);
});
});
// move up/down
openDropdown();
// focus on first item
type(`{upArrow}`);
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.eq(0)
.contains(optionsWithCustom[0].value);
});
// focus on custom
type(`{downArrow}`);
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.eq(0)
.within(() => {
cy.get(`input`)
.invoke('val')
.should('eq', customValue);
});
});
});
});
});
});
describe('Input text', () => {
describe('onTextChange', () => {
it('should fire', () => {
const onTextChangeSpy = cy.spy().as('onTextChangeSpy');
cy.mount(<Wrapper options={options} onTextChange={onTextChangeSpy} />);
const text = 'abc';
type(text);
assertInputValue(text);
cy.get('@onTextChangeSpy').should('have.been.calledWith', text);
});
});
describe('inputText', () => {
it('should call onTextChange on mount', () => {
const text = '';
const onTextChangeSpy = cy.spy().as('onTextChangeSpy');
const value = options[1];
cy.mount(
<Wrapper
options={options}
inputText={text}
value={value}
onTextChange={onTextChangeSpy}
/>
);
assertInputValue(value.label);
cy.get('@onTextChangeSpy').should(
'have.been.calledOnceWith',
value.label
);
});
describe('default value', () => {
it('should set as default value', () => {
const text = 'abc';
cy.mount(<Wrapper options={options} inputText={text} />);
assertInputValue(text);
});
it('should prefer inputText over value.label', () => {
const text = 'abc';
const value = options[1];
cy.mount(
<Wrapper options={options} inputText={text} value={value} />
);
assertInputValue(text);
});
it('should use value.label if inputText empty', () => {
const text = '';
const value = options[1];
cy.mount(
<Wrapper options={options} inputText={text} value={value} />
);
assertInputValue(value.label);
});
});
it('should select value from list', () => {
const onTextChangeSpy = cy.spy().as('onTextChangeSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = options[1];
const newValue = options[2];
const text = value.label;
cy.mount(
<Wrapper
options={options}
inputText={text}
value={value}
onTextChange={onTextChangeSpy}
onChange={onChangeSpy}
/>
);
openDropdown();
assertDropdownOpen().within(() => {
cy.contains(newValue.label).click();
});
assertInputValue(newValue.label);
cy.get('@onTextChangeSpy').should(
'have.been.calledOnceWith',
value.label
);
cy.get('@onChangeSpy').should('have.been.calledWith', newValue);
});
it.skip('onBlur, can not properly test it for now', () => {});
});
});
describe('should update Select input if prop changed', () => {
it('inputText', () => {
const onTextChangeSpy = cy.spy().as('onTextChangeSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = options[1];
const text = value.label;
cy.mount(
<Wrapper
options={options}
inputText={text}
value={value}
onTextChange={onTextChangeSpy}
onChange={onChangeSpy}
/>
);
getInputText().should('have.value', text);
assertInputValue(text);
cy.get('@onTextChangeSpy').should(
'have.been.calledOnceWith',
value.label
);
cy.get('@onChangeSpy').should('not.have.been.called');
clearInputText();
changeInputText('12345');
assertInputValue('12345');
cy.get('@onTextChangeSpy').should(
'have.been.calledOnceWith',
value.label
);
cy.get('@onChangeSpy').should('not.have.been.called');
});
it('value', () => {
const onTextChangeSpy = cy.spy().as('onTextChangeSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const value = options[1];
const text = value.label;
cy.mount(
<Wrapper
options={options}
inputText={text}
value={value}
onTextChange={onTextChangeSpy}
onChange={onChangeSpy}
/>
);
getInputText().should('have.value', text);
assertInputValue(text);
cy.get('@onTextChangeSpy').should(
'have.been.calledOnceWith',
value.label
);
cy.get('@onChangeSpy').should('not.have.been.called');
const newValue = options[2];
changeValue(newValue);
assertInputValue(newValue.label);
cy.get('@onTextChangeSpy').should(
'have.been.calledOnceWith',
value.label
);
cy.get('@onChangeSpy').should('not.have.been.called');
});
});
describe('autoFocus', () => {
it('focused by default', () => {
cy.mount(<Wrapper options={options} renderInput={renderInput} />);
assertDropdownClosed();
cy.get('[data-testid="props"]').within(() => {
cy.get('[data-testid="autoFocus"]').contains('true');
});
});
it('should be false when passed', () => {
cy.mount(
<Wrapper
autoFocus={false}
options={options}
renderInput={renderInput}
/>
);
cy.get('[data-testid="props"]').within(() => {
cy.get('[data-testid="autoFocus"]').contains('false');
});
});
});
describe('clear icon', () => {
const value = options[0];
it('hidden by default', () => {
cy.mount(<Wrapper options={options} value={value} />);
assertDropdownClosed();
cy.get(SELECTORS.close).should('not.exist');
});
it('should work', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
showClearIcon={true}
options={options}
value={value}
onChange={onChangeSpy}
/>
);
cy.get(SELECTORS.close).should('exist');
cy.get(SELECTORS.wrapper).realHover();
cy.get(SELECTORS.close)
.should('be.visible')
.click();
cy.get('@onChangeSpy').should('have.been.calledOnceWith', undefined);
});
});
describe('Toggle icon', () => {
it('visible by default', () => {
cy.mount(<Wrapper options={options} />);
assertDropdownClosed();
cy.get(SELECTORS.toggle).should('be.visible');
});
it('should be hidden when showToggleIcon=false', () => {
cy.mount(<Wrapper options={options} showToggleIcon={false} />);
cy.get(SELECTORS.toggle).should('not.exist');
});
});
describe('Custom components', () => {
const backgroundColor = 'rgb(255, 0, 0)';
const customId = 'myCustomId123';
describe('renderInput - custom text input', () => {
it('should pass props', () => {
const value = options[0];
cy.mount(
<Wrapper
options={options}
value={value}
name="test name"
label="test label"
disabled={false}
renderInput={renderInput}
/>
);
cy.get('[data-testid="CustomSelect-text-field"]').should('not.exist');
cy.get('[data-testid="customInput1"]').should('be.visible');
cy.get('[data-testid="props"]').within(() => {
cy.get('[data-testid="value"]').contains(value.label);
cy.get('[data-testid="onChange"]').contains('func');
cy.get('[data-testid="onClick"]').contains('func');
cy.get('[data-testid="inputRef"]').contains('{}');
cy.get('[data-testid="onFocus"]').contains('func');
cy.get('[data-testid="autoFocus"]').contains('true');
cy.get('[data-testid="onKeyDown"]').contains('func');
cy.get('[data-testid="name"]').contains('test name');
cy.get('[data-testid="label"]').contains('test label');
cy.get('[data-testid="disabled"]').contains('false');
cy.get('[data-testid="className"]').contains('textField');
});
});
});
describe('renderItem - custom listItem component', () => {
const renderItem = ({ selected, focused, label }) => {
return (
<div className="custom">
<div className="selected">{JSON.stringify(selected)}</div>
<div className="focused">{JSON.stringify(focused)}</div>
<div className="label">{JSON.stringify(label)}</div>
</div>
);
};
it('should render', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
renderItem={renderItem}
options={options}
onChange={onChangeSpy}
/>
);
openDropdown();
assertDropdownOpen().within(() => {
assertListLength(options.length);
assertFocusedSelected();
});
});
it('should focus', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
renderItem={renderItem}
options={options}
onChange={onChangeSpy}
/>
);
openDropdown();
type('{downArrow}');
assertDropdownOpen().within(() => {
assertListLength(options.length);
assertFocusedSelected({
focused: 0,
});
});
type('{downArrow}');
assertFocusedSelected({
focused: 1,
});
type('{downArrow}');
assertFocusedSelected({
focused: 2,
});
type('{downArrow}');
assertFocusedSelected({
focused: 3,
});
type('{upArrow}');
assertFocusedSelected({
focused: 2,
});
type('{upArrow}');
assertFocusedSelected({
focused: 1,
});
type('{upArrow}');
assertFocusedSelected({
focused: 0,
});
});
it('should select', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<Wrapper
renderItem={renderItem}
options={options}
onChange={onChangeSpy}
/>
);
const value = options[1];
openDropdown();
assertDropdownOpen().within(() => {
cy.contains(value.label).click();
});
cy.get('@onChangeSpy').should('have.been.calledWith', value);
assertInputValue(value.label);
assertDropdownClosed();
});
});
describe('PopperComponent - custom Popper', () => {
it('should work', () => {
const StyledPopper = styled(Popper)(() => ({
backgroundColor,
}));
const CustomPopper = (props = {}) => {
return <StyledPopper {...props} data-testid={customId} />;
};
cy.mount(<Wrapper options={options} PopperComponent={CustomPopper} />);
openDropdown();
assertDropdownOpen();
cy.get(`[data-testid="${customId}"]`)
.should('be.visible')
.should('have.css', 'background-color', backgroundColor);
});
});
describe('PaperComponent - custom Paper', () => {
it('should work', () => {
const StyledPaper = styled(Paper)(() => ({
backgroundColor,
}));
const CustomPaper = (props = {}) => {
return <StyledPaper {...props} className={customId} />;
};
cy.mount(<Wrapper options={options} PaperComponent={CustomPaper} />);
openDropdown();
assertDropdownOpen();
cy.get(`.${customId}`)
.should('be.visible')
.should('have.css', 'background-color', backgroundColor);
});
});
describe('renderListTitle - custom Title', () => {
it('should work', () => {
const StyledTitle = styled('div')(() => ({
backgroundColor,
}));
const text = 'TestTitle12222';
const CustomTitle = ({ headerTitle }) => {
return <StyledTitle className={customId}>{headerTitle}</StyledTitle>;
};
cy.mount(
<Wrapper
options={options}
renderListTitle={CustomTitle}
headerTitle={text}
/>
);
openDropdown();
assertDropdownOpen();
cy.get(`.${customId}`)
.should('be.visible')
.should('have.css', 'background-color', backgroundColor)
.should('contain', text);
});
});
});
describe('onUpdate', () => {
it('should update state', () => {
const onUpdateSpy = cy.spy().as('onUpdateSpy');
cy.mount(<Wrapper options={options} onUpdate={onUpdateSpy} />);
cy.wrap(onUpdateSpy?.args).then(calls => {
cy.wrap(
calls.map(call => ({
isOpen: call?.[0]?.isOpen,
focused: call?.[0]?.focused,
}))
).should('deep.equal', [{ isOpen: false, focused: null }]);
});
openDropdown();
assertDropdownOpen();
cy.wrap(onUpdateSpy?.args).then(calls => {
cy.wrap(
calls.map(call => ({
isOpen: call?.[0]?.isOpen,
focused: call?.[0]?.focused,
}))
).should('deep.equal', [
{ isOpen: false, focused: null },
{ isOpen: true, focused: null },
]);
});
type('{downArrow}');
cy.wrap(onUpdateSpy?.args).then(calls => {
cy.wrap(
calls.map(call => ({
isOpen: call?.[0]?.isOpen,
focused: call?.[0]?.focused,
}))
).should('deep.equal', [
{ isOpen: false, focused: null },
{ isOpen: true, focused: null },
{ isOpen: true, focused: { label: '100mi', value: '100' } },
]);
});
closeDropdown();
assertDropdownClosed();
cy.wrap(onUpdateSpy?.args).then(calls => {
cy.wrap(
calls.map(call => ({
isOpen: call?.[0]?.isOpen,
focused: call?.[0]?.focused,
}))
).should('deep.equal', [
{ isOpen: false, focused: null },
{ isOpen: true, focused: null },
{
isOpen: true,
focused: { label: '100mi', value: '100' },
},
{
isOpen: false,
focused: { label: '100mi', value: '100' },
},
{ isOpen: false, focused: null },
]);
});
});
});
describe('getInitialFocused', () => {
it('should return which item focused', () => {
const focused = options[1];
const getInitialFocused = ({ options }) => focused;
cy.mount(
<Wrapper options={options} getInitialFocused={getInitialFocused} />
);
openDropdown();
assertDropdownOpen().within(() => {
cy.get(SELECTORS.listItem)
.filter('[data-focus="true"]')
.should('have.length', 1)
.should('contain', focused.label);
});
});
});
});
const renderInput = props => (
<div data-testid="props">
{Object.entries(props).map(([name, value], ind) => (
<div data-testid={name} key={ind}>
{name}: {typeof value === 'function' ? 'func' : JSON.stringify(value)}
</div>
))}
<input data-testid="customInput1" />
</div>
);
function Wrapper({
value: _value,
onChange = () => false,
inputText: _inputText,
...rest
}) {
const [value, setValue] = React.useState(_value);
const [inputText, setInputText] = React.useState(_inputText);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<div
style={{
padding: '20px',
backgroundColor: 'grey',
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
padding: '10px',
width: '200px',
backgroundColor: 'white',
}}
>
<div data-testid="test">
<input autoFocus={false} />
</div>
<div data-testid="other">other</div>
<CustomSelect
{...rest}
onChange={val => {
setValue(val);
onChange(val);
}}
value={value}
inputText={inputText}
/>
<div style={{ margin: '20px 0' }}>
<input
data-testid="inputText"
onChange={ev => setInputText(ev?.target?.value)}
value={inputText}
autoFocus={false}
/>
</div>
<div style={{ margin: '20px 0' }}>
<input
data-testid="value"
onChange={ev => {
let isValidJson = false;
let jsonValue = '';
try {
jsonValue = JSON.parse(ev?.target?.value);
isValidJson = true;
} catch (err) {}
if (isValidJson) {
setValue(jsonValue);
}
}}
autoFocus={false}
/>
</div>
</div>
</div>
</ThemeProvider>
);
}
const changeValue = (valueObj = {}) => {
cy.log('changeValue');
const text = JSON.stringify(valueObj);
return getValueText()
.clear()
.type(text, { parseSpecialCharSequences: false });
};
const changeInputText = (text = '') => {
cy.log('changeInputText');
return getInputText().type(text);
};
const clearInputText = () => {
cy.log('clearInputText');
getInputText().clear();
};
const getInputText = () => {
return cy.get('[data-testid="inputText"]');
};
const getValueText = () => {
return cy.get('[data-testid="value"]');
};
const assertFocusedSelected = ({ focused = -1, selected = -1 } = {}) => {
cy.log(`assertFocusedSelected focused:${focused}, selected:${selected}`);
return cy.wrap(options).each((opt, ind) => {
assertListItem({
num: ind,
focused: ind === focused,
selected: selected === ind,
label: opt.label,
});
});
};
const assertListItem = ({ num = 0, focused, selected, label }) => {
cy.log(`assertListItem numb:${num}`);
return cy
.get(SELECTORS.listItem)
.eq(num)
.within(() => {
cy.get('.selected').contains(String(selected));
cy.get('.focused').contains(String(focused));
cy.get('.label').contains(label);
});
};
const assertDropdownOpen = () => {
cy.log('assertDropdownOpen');
return cy.get(SELECTORS.paper).should('be.visible');
};
const assertDropdownClosed = () => {
cy.log('assertDropdownClosed');
return cy.get(SELECTORS.paper).should('not.exist');
};
const assertCustomComponentVisible = () => {
cy.log('assertCustomComponentVisible');
return cy.get(SELECTORS.customInputWrapper).should('be.visible');
};
const assertCustomComponentFocused = () => {
cy.log('assertCustomComponentFocused');
return cy.get(SELECTORS.customInput).should('be.focused');
};
const assertCustomComponentNotFocused = () => {
cy.log('assertCustomComponentNotFocused');
return cy.get(SELECTORS.customInput).should('not.be.focused');
};
const assertListLength = (length = 0) => {
cy.log(`assertListLength ${0}`);
return cy.get(SELECTORS.listItem).should('have.length', length);
};
const assertCustomComponentValue = (value = '') => {
cy.log(`assertCustomComponentValue ${value}`);
return cy
.get(SELECTORS.customInput)
.invoke('val')
.should('eq', value);
};
const assertInputValue = (value = '') => {
cy.log(`assertInputValue ${value}`);
return cy
.get(SELECTORS.textFieldInput)
.invoke('val')
.should('eq', value);
};
const clearInput = () => {
cy.log('clearInput');
return cy.get(SELECTORS.textFieldInput).clear();
};
const type = (text = '') => {
cy.log(`type ${text}`);
return cy.get(SELECTORS.textFieldInput).type(text);
};
const typeCustom = (text = '') => {
cy.log(`typeCustomInput ${text}`);
cy.get(SELECTORS.customInput).clear();
return cy.get(SELECTORS.customInput).type(text);
};
const closeDropdown = () => {
cy.log('closeDropdown');
return cy.get(SELECTORS.otherElement).click();
};
const closeDropdownByEsc = () => {
cy.log('closeDropdownByEsc');
return cy.get(SELECTORS.textFieldInput).type(`{esc}`);
};
const openDropdown = () => {
cy.log('openDropdown');
return cy.get(SELECTORS.textFieldInput).click();
};
const toggeDropdown = () => {
cy.log('toggeDropdown');
return cy.get(SELECTORS.toggle).click();
};
import React from 'react';
import _ from 'lodash/fp';
import clsx from 'clsx';
import { sortClosestTo } from './common';
import { useClickOutside, useFocusOutside } from 'hooks/useClickOutside';
import { usePrevious } from './usePrevious';
import { BoundariesContext } from './boundaryContext';
import {
Wrapper,
ListItem,
Listbox,
StyledItemWrapper,
NoOptionsStub,
Buttons,
renderInputComponent,
CustomPaperComponent,
CustomPopperComponent,
CustomListTitleComponent,
} from './CustomSelect.styles';
export const CustomSelect = ({
value,
inputText,
onChange = _.noop,
onBlur = _.noop,
onTextChange = _.noop,
onKeyDown: onKeyDownProp,
className,
options = [],
fuzzy,
label = '',
name = '',
headerTitle = '',
disabled,
hasBorder = true,
hideNoOptions = true,
openOnFocus = true,
autoFocus = true,
showToggleIcon = true,
showClearIcon = false,
filterOptions = defaultFilterOptions,
StartIcon,
renderItem,
renderInput,
renderListTitle,
PopperComponent,
PaperComponent,
dataTestId,
onUpdate = _.noop,
getInitialFocused,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const { text, setText } = useText({
textProp: inputText,
value,
});
useTextUpdate({
inputText,
value,
isOpen,
setText,
});
const handleOpen = React.useCallback(() => setIsOpen(true), []);
const handleClose = React.useCallback(() => {
setIsOpen(false);
}, []);
const handleToggle = React.useCallback(ev => {
ev.preventDefault();
ev.stopPropagation();
setIsOpen(v => !v);
}, []);
const wrapperRef = React.useRef();
const inputRef = React.useRef();
const paperRef = React.useRef();
const listboxRef = React.useRef();
const refs = React.useMemo(() => [wrapperRef, inputRef, paperRef], []);
useClickOutside(handleClose, refs, 50);
useFocusOutside(handleClose, refs, 50);
const handleTextChange = React.useCallback(
ev => {
const value = ev?.target?.value || '';
setText({ value, reason: 'type' });
onTextChange(value);
handleOpen();
},
[handleOpen, onTextChange, setText]
);
useOnMountTextChange(text?.value, onTextChange);
const handleFocus = React.useCallback(
ev => {
ev.target.select();
if (openOnFocus) {
handleOpen();
}
},
[handleOpen, openOnFocus]
);
const returnFocus = React.useCallback(() => {
inputRef.current?.focus?.();
}, []);
const { optionsList } = useOptions({
text: text?.value,
options,
isOpen,
fuzzy,
filterOptions,
});
const handleSelect = React.useCallback(
opt => {
setText({ value: opt?.label || '', reason: 'select' });
onChange(opt);
handleClose();
},
[handleClose, onChange, setText]
);
const handleClear = React.useCallback(() => {
setText({ value: '', reason: 'clear' });
onChange();
}, [onChange, setText]);
const { onKeyDown, focused: focusedItem } = useKeys({
handleClose,
options: optionsList,
onKeyDownProp,
isOpen,
value,
onSelect: handleSelect,
getInitialFocused,
});
const selectedItem = optionsList.find(opt => isEqual(opt, value));
const boundariesElement = React.useContext(BoundariesContext);
const haveTitle = !!headerTitle;
const haveOptions = !!optionsList.length;
useEmitState({
onUpdate,
isOpen,
focused: focusedItem,
handleClose,
});
return (
<Wrapper
ref={wrapperRef}
hasborder={+hasBorder}
className={`${className} wrapper`}
data-testid={dataTestId || 'Select'}
aria-expanded={isOpen}
>
{renderInputComponent({
renderInput,
onBlur,
value: text?.value,
autoFocus,
onChange: handleTextChange,
onClick: handleOpen,
inputRef,
onFocus: handleFocus,
onKeyDown,
name,
label,
disabled,
StartIcon,
})}
<Buttons
isOpen={isOpen}
onToggle={showToggleIcon && handleToggle}
onClear={showClearIcon && handleClear}
disabled={disabled}
/>
<CustomPopperComponent
PopperComponent={PopperComponent}
open={!disabled && isOpen}
boundariesElement={boundariesElement}
wrapperRef={wrapperRef}
>
<CustomPaperComponent
PaperComponent={PaperComponent}
innerRef={paperRef}
>
<CustomListTitleComponent
renderListTitle={renderListTitle}
headerTitle={headerTitle}
/>
{!hideNoOptions && !haveOptions && <NoOptionsStub />}
{haveOptions && (
<Listbox
role="listbox"
data-testid="CustomSelect-options-wrapper"
className={`listbox ${haveTitle ? 'haveTitle' : ''}`}
ref={listboxRef}
>
{optionsList.map(opt => {
const isFocused = isEqual(focusedItem, opt);
const isSelected = isEqual(selectedItem, opt);
const isCustom = isCustomOption(opt);
const key = opt?.id || opt?.value || opt?.label;
return (
<ItemWrapper
key={key}
selected={isSelected}
focused={isFocused}
isCustom={isCustom}
listboxRef={listboxRef}
onClick={() => {
if (!isCustom) {
onChange(opt);
handleClose();
}
}}
>
{isCustom ? (
<opt.Component
headerTitle={opt?.headerTitle}
selected={isSelected}
focused={isFocused}
returnFocus={returnFocus}
value={isSelected && value}
wrapperRef={wrapperRef}
onChange={value => {
onChange({
...omitCustomProps(opt),
label: opt?.labelFormatter?.(value) || value,
value,
});
}}
close={handleClose}
onKeyDown={onKeyDown}
className="listItem customListItem"
/>
) : (
<ListItem
selected={isSelected}
focused={isFocused}
label={opt?.label}
renderItem={renderItem}
text={text?.value}
opt={opt}
/>
)}
</ItemWrapper>
);
})}
</Listbox>
)}
</CustomPaperComponent>
</CustomPopperComponent>
</Wrapper>
);
};
export const omitCustomProps = _.omit([
'Component',
'headerTitle',
'labelFormatter',
]);
export const isCustomOption = opt =>
String(opt?.id).includes('custom') || !!opt?.Component;
const isEqual = (opt1, opt2) =>
!!opt1?.id && !!opt2?.id
? opt1?.id === opt2?.id
: opt1?.value === opt2?.value;
const useText = ({ textProp, value }) => {
const [text, setText] = React.useState({
value: textProp || value?.label || '',
reason: 'prop-initial',
});
return { text, setText };
};
const useEmitState = ({ onUpdate, ...state }) => {
const statePrev = usePrevious(state);
if (typeof onUpdate !== 'function') {
return false;
}
if (JSON.stringify(state) !== JSON.stringify(statePrev)) {
onUpdate(state);
}
};
const useTextUpdate = ({ inputText, value, isOpen, setText }) => {
const prevInputText = usePrevious(inputText);
const inputTextChanged =
typeof prevInputText === 'string' &&
typeof inputText === 'string' &&
inputText !== prevInputText;
React.useEffect(() => {
if (inputTextChanged) {
setText({ value: inputText, reason: 'prop-inputText' });
}
}, [inputText, inputTextChanged, setText]);
const isOpenPrev = usePrevious(isOpen);
React.useEffect(() => {
if (!!value && !isOpen && isOpenPrev) {
setText({ value: value?.label || '', reason: 'close' });
}
}, [value, isOpen, setText, isOpenPrev]);
const valuePrev = usePrevious(value);
const valueChanged = !!value && !!valuePrev && !_.isEqual(value, valuePrev);
React.useEffect(() => {
if (valueChanged) {
setText({ value: value?.label || '', reason: 'prop-value' });
}
}, [valueChanged, value, setText]);
};
const useOnMountTextChange = (text, onTextChange) => {
React.useEffect(() => {
onTextChange(text);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};
const useKeys = ({
onKeyDownProp,
value,
handleClose,
isOpen,
options,
onSelect,
getInitialFocused,
}) => {
const selectedVisible = !!options.find(opt => isEqual(opt, value));
const initialFocused = getInitialFocused?.({ options }) || null;
const defaultFocused = selectedVisible ? value : initialFocused;
const [focused, setFocused] = React.useState(defaultFocused);
React.useEffect(() => {
if (!isOpen || !!defaultFocused) {
setFocused(defaultFocused);
}
}, [defaultFocused, isOpen]);
const [min, max] = [0, options.length - 1];
const onKeyDown = ev => {
const key = ev?.key;
const shouldSkipStandartHandler = onKeyDownProp?.(ev, {
key,
focused,
setFocused,
isOpen,
handleClose,
});
if (shouldSkipStandartHandler) {
return false;
}
let ind = options.findIndex(opt => isEqual(opt, focused));
if (key === 'Escape') {
ev.preventDefault();
handleClose();
} else if (key === 'ArrowUp') {
ev.preventDefault();
if (ind === -1) {
ind = min + 1;
}
const newInd = ind - 1 >= min ? ind - 1 : min;
setFocused(options[newInd]);
} else if (key === 'ArrowDown') {
ev.preventDefault();
const newInd = ind + 1 < max ? ind + 1 : max;
setFocused(options[newInd]);
} else if (key === 'Enter' && isOpen) {
ev.preventDefault();
onSelect(omitCustomProps(focused));
}
};
return { onKeyDown, focused };
};
const useOptions = ({ text, options, isOpen, fuzzy, filterOptions }) => {
const isOpenPrev = usePrevious(isOpen);
const [shouldFilter, setShouldFilter] = React.useState(false);
React.useEffect(() => {
if (isOpen && !isOpenPrev) {
setShouldFilter(false);
return _.noop;
}
if (isOpen) {
setShouldFilter(!!text);
} else {
setShouldFilter(false);
}
}, [text, isOpen, isOpenPrev]);
const filtered = shouldFilter
? options.filter(opt => filterOptions({ opt, text }))
: options;
const optionsList =
fuzzy === 'numbers' ? sortFuzzy({ text, options, filtered }) : filtered;
return { optionsList };
};
const sortFuzzy = ({ filtered, options, text }) => {
const shouldSelectClosest = !filtered.length;
const value = parseInt(text);
const sorted = shouldSelectClosest
? sortClosestTo(
options.filter(opt => !isCustomOption(opt)),
value,
val => val?.value
)
: filtered;
const result = shouldSelectClosest ? sorted.slice(0, 1) : sorted;
return result;
};
const ItemWrapper = ({
children,
onClick,
isCustom,
selected,
focused,
listboxRef,
}) => {
const ref = React.useRef();
React.useEffect(() => {
if (selected || focused) {
scrollToItem({ containerRef: listboxRef, itemRef: ref });
}
}, [selected, focused, listboxRef]);
return (
<StyledItemWrapper
ref={ref}
tabIndex={-1}
className={clsx(
!isCustom && 'option',
selected && 'selected',
'itemWrapper'
)}
role={isCustom ? undefined : 'option'}
aria-selected={selected}
data-focus={focused}
onClick={onClick}
>
{children}
</StyledItemWrapper>
);
};
const scrollToItem = ({ containerRef, itemRef }) => {
const itemExist = typeof itemRef.current?.scrollTop === 'number';
const listboxExist = typeof containerRef.current?.scrollTop === 'number';
const noScrol =
containerRef.current?.scrollHeight === containerRef.current?.clientHeight;
if (noScrol || !itemExist || !listboxExist) {
return false;
}
// if have title on list
const listOffset = containerRef.current?.offsetTop || 0;
const listTop = containerRef.current?.scrollTop;
const listBottom = listTop + containerRef.current?.clientHeight;
const itemTop = itemRef.current?.offsetTop - listOffset;
const itemHeight = itemRef.current?.clientHeight;
const itemBottom = itemTop + itemHeight;
const itemVisible = itemBottom <= listBottom && itemTop >= listTop;
if (itemVisible) {
return false;
}
containerRef.current.scrollTop = itemTop;
};
const defaultFilterOptions = ({ opt, text }) =>
String(opt?.label)
.toLowerCase()
.includes(String(text).toLocaleLowerCase());
import React from 'react';
import { styled } from '@material-ui/core/styles';
import {
ArrowDropDown as ArrowDownIcon,
ArrowDropUp as ArrowUpIcon,
Close as MuiCloseIcon,
} from '@material-ui/icons';
import { InputAdornment } from '@material-ui/core';
import { IconButton } from '@material-ui/core';
import { Paper } from 'roles/member/pages/WhatsMyCopaySearch/components/Paper';
import { Popper as BasePopper } from 'roles/member/pages/WhatsMyCopaySearch/components/Popper';
import { PADDING } from 'roles/member/pages/WhatsMyCopaySearch/components/Paper';
import { TextField as MuiTextField } from 'roles/member/pages/WhatsMyCopaySearch/components/TextField';
import { IconContainer } from 'roles/member/pages/WhatsMyCopaySearch/components/IconContainer';
import { CheckmarkIcon } from 'roles/member/pages/WhatsMyCopaySearch/components/CheckmarkIcon';
export const Wrapper = styled('div')(({ theme, hasborder }) => ({
padding: '0 12px',
position: 'relative',
display: 'flex',
flexWrap: 'nowrap',
border: `1px solid ${
hasborder ? theme.updPalette.border.main : 'transparent'
}`,
borderRadius: '10px',
'& .clear': {
visibility: 'hidden',
},
'&:hover': {
'& .clear': {
visibility: 'visible',
},
},
'&:focus-within': {
border: `1px solid ${
hasborder ? theme.updPalette.border.focused : 'transparent'
}`,
},
height: '63px',
}));
const StartIconContainer = ({ StartIcon }) => {
if (!StartIcon) {
return null;
}
return (
<InputAdornment
position="start"
css={{ height: '100%', alignItems: 'center', marginRight: '4px' }}
>
<IconContainer size="14">
<StartIcon />
</IconContainer>
</InputAdornment>
);
};
export const Listbox = styled('ul')(() => ({
overflowY: 'auto',
'&.haveTitle': {
paddingTop: 0,
},
}));
const TextField = ({
value,
onChange,
onClick,
disabled,
name,
label,
inputRef,
onFocus,
onKeyDown,
StartIcon,
}) => {
return (
<StyledTextField
value={value}
onChange={onChange}
onClick={onClick}
inputRef={inputRef}
onFocus={onFocus}
onKeyDown={onKeyDown}
name={name}
label={label}
disabled={disabled}
fullWidth
variant="outlined"
InputLabelProps={inputLabelProps}
data-testid="CustomSelect-text-field"
className={`Select_component ${name}`}
InputProps={{
startAdornment: <StartIconContainer StartIcon={StartIcon} />,
}}
/>
);
};
const StyledTextField = styled(MuiTextField)(({ theme }) => ({
height: '100%',
'& label.MuiInputLabel-outlined': {
color: theme.updTypography.placeholder.main.color,
transform: 'translateX(0px) translateY(20px)',
'&.MuiInputLabel-shrink.Mui-focused, &.MuiFormLabel-filled': {
transform: 'translateX(0px) translateY(10px) scale(0.75)',
},
},
'& .MuiInputBase-root': {
height: '100%',
padding: 0,
border: 'none!important',
'& .MuiInput-underline:before,.MuiInput-underline:after': {
display: 'none!important',
},
'& [class*="NotchedOutline"], .MuiOutlinedInput-notchedOutline': {
display: 'none!important',
opacity: '0!important',
},
'& input.MuiInputBase-input': {
padding: '0',
transform: 'translateY(12px)',
},
},
}));
function onDragStart(ev) {
ev.preventDefault();
return false;
}
const defaultInputProps = {
autoComplete: 'off',
autoCapitalize: 'off',
className: 'textField',
spellCheck: false,
onDragStart,
};
export const renderInputComponent = ({
renderInput,
onBlur,
value,
autoFocus,
onChange,
onClick,
inputRef,
onFocus,
onKeyDown,
name,
label,
disabled,
StartIcon,
}) => {
const Props = {
...defaultInputProps,
onBlur,
value,
autoFocus,
onChange,
onClick,
inputRef,
onFocus,
onKeyDown,
name,
label,
disabled,
StartIcon,
};
if (typeof renderInput === 'function') {
return renderInput(Props);
}
return <TextField {...Props} />;
};
const Popper = styled(BasePopper)(() => ({
zIndex: '1500',
'& .MuiPaper-root': {
maxHeight: '400px',
overflowY: 'auto',
},
}));
export const CustomPopperComponent = ({
PopperComponent,
open,
boundariesElement,
wrapperRef,
children,
}) => {
const minWidth = Math.max(wrapperRef.current?.clientWidth || 0, 150);
const Props = {
open,
anchorEl: wrapperRef.current,
boundariesElement,
disablePortal: true,
css: { minWidth },
};
if (typeof PopperComponent === 'function') {
return <PopperComponent {...Props}>{children}</PopperComponent>;
}
return <Popper {...Props}>{children}</Popper>;
};
export const CustomPaperComponent = ({
PaperComponent,
innerRef,
children,
}) => {
const Props = {
'data-testid': 'CustomSelect-paper',
innerRef,
className: 'paper',
};
if (typeof PaperComponent === 'function') {
return <PaperComponent {...Props}>{children}</PaperComponent>;
}
return <Paper {...Props}>{children}</Paper>;
};
export const Buttons = ({ isOpen, onToggle, onClear, disabled }) => {
return (
<ButtonsWrapper tabIndex="-1" className="CustomSelect-toggle-container">
{!!onClear && <ClearButton onClick={onClear} disabled={disabled} />}
{!!onToggle && (
<ToggleButton onClick={onToggle} isOpen={isOpen} disabled={disabled} />
)}
</ButtonsWrapper>
);
};
const ButtonsWrapper = styled('div')(() => ({
position: 'absolute',
right: '4px',
height: '100%',
maxWidth: '48px',
display: 'flex',
alignItems: 'center',
}));
const ButtonContainer = styled(IconButton)(() => ({
'&': {
width: '24px',
height: '24px',
padding: 0,
'& svg, .MuiIconButton-label': {
width: '14px',
height: '14px',
},
},
}));
const ClearButton = ({ onClick, disabled }) => {
return (
<ButtonContainer
onClick={onClick}
disabled={disabled}
tabIndex="-1"
className="clear"
aria-label="Clear"
>
<CloseIcon />
</ButtonContainer>
);
};
const CloseIcon = () => (
<IconContainer
data-testid="CustomSelect-close"
size="14"
className="classIcon"
>
<MuiCloseIcon />
</IconContainer>
);
const ToggleButton = ({ onClick, isOpen, disabled }) => {
return (
<ButtonContainer
onClick={onClick}
disabled={disabled}
tabIndex="-1"
aria-label="Toggle"
>
<ToggleIcon isOpen={isOpen} />
</ButtonContainer>
);
};
const ToggleIcon = ({ isOpen }) => {
return (
<IconContainer
data-testid="CustomSelect-toggle"
size="14"
className="toggleIcon"
>
{isOpen ? <ArrowUpIcon /> : <ArrowDownIcon />}
</IconContainer>
);
};
const ListTitle = React.memo(({ text = '' }) => {
return (
<TitleWrapper className="listTitle" data-testid="CustomSelect-title">
{text}
</TitleWrapper>
);
});
const TitleWrapper = styled('div')(({ theme }) => ({
textTransform: 'uppercase',
marginLeft: PADDING,
paddingTop: PADDING,
paddingBottom: '12px',
color: theme.updTypography.h4.color,
fontWeight: 500,
fontSize: '11px',
lineHeight: '14px',
}));
export const CustomListTitleComponent = ({ renderListTitle, headerTitle }) => {
if (typeof renderListTitle === 'function') {
return renderListTitle({ headerTitle });
}
return !!headerTitle && <ListTitle text={headerTitle} />;
};
export const NoOptionsStub = () => {
return (
<NoOptionsWrapper
data-testid="CustomSelect-no-options"
className="noOptions"
>
No options
</NoOptionsWrapper>
);
};
const NoOptionsWrapper = styled('div')(() => ({
padding: '24px',
}));
export const StyledItemWrapper = styled('li')(() => ({
userSelect: 'none',
userDrag: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
'& .checkmark': {
visibility: 'hidden',
},
'&.selected .checkmark': {
visibility: 'visible',
},
}));
export const StyledListItem = React.memo(({ label = '' }) => {
return (
<ListItemWrapper className="listItem">
{label}
<StyledIconContainer className="checkmark">
<CheckmarkIcon />
</StyledIconContainer>
</ListItemWrapper>
);
});
export const ListItem = ({
renderItem,
selected,
focused,
label,
text,
opt,
}) => {
if (typeof renderItem === 'function') {
return renderItem({ selected, focused, label, text, option: opt });
}
return <StyledListItem label={label} />;
};
const StyledIconContainer = styled(IconContainer)(({ theme }) => ({
color: theme.updPalette.primary.main,
}));
const ListItemWrapper = styled('div')(() => ({
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}));
const inputLabelProps = {
disableAnimation: true,
};
import React from 'react';
import _ from 'lodash/fp';
import { useTimeout } from 'hooks/useTimeout';
export const useClickOutside = (cb = _.noop, refs, delay = 0) => {
const cbHandler = useTimeout(cb, delay);
React.useEffect(() => {
const handleClick = ev => {
const isOutside = refs
.filter(r => !!r.current)
.every(r => !r.current.contains(ev.target));
if (isOutside) {
cbHandler();
}
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('touchstart', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('touchstart', handleClick);
};
}, [refs, cbHandler]);
};
export const useFocusOutside = (cb = _.noop, refs, delay = 0) => {
const cbHandler = useTimeout(cb, delay);
React.useEffect(() => {
const handleClick = ev => {
const isOutside = refs
.filter(r => !!r.current)
.every(r => !r.current.contains(ev.target));
if (isOutside) {
cbHandler();
}
};
document.addEventListener('focusin', handleClick);
return () => {
document.removeEventListener('focusin', handleClick);
};
}, [refs, cbHandler]);
};
import React from 'react';
export const usePrevious = value => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
/**
* Sort array of numbers by closest to value
* @template T array of numbers or object with numeric property
* @param {T[]} data Arr for sorting
* @param {Number} value Value to sort by
* @param {(arg: T) => number} getter Fn to get numeric prop of an object
* @param {(arg: T) => boolean} filter Fn to filter options before sort
* @returns {T[]} `T[]`
*/
export const sortClosestTo = (
data,
value,
getter = val => val,
filter = false
) => {
if (!isNumber(value) || !Array.isArray(data)) {
return [];
}
const filteredData =
typeof filter === 'function' ? data.filter(filter) : data;
return filteredData.sort(
(a, b) => Math.abs(+getter(a) - +value) - Math.abs(+getter(b) - +value)
);
};
import { sortClosestTo } from './utils';
describe('sortClosestTo', () => {
it('should return array sorted by closest to value', () => {
const arr = [1, 2, 3, 4, '5', 6, 7];
expect(sortClosestTo(arr, 0)).toStrictEqual([1, 2, 3, 4, '5', 6, 7]);
expect(sortClosestTo(arr, 1)).toStrictEqual([1, 2, 3, 4, '5', 6, 7]);
expect(sortClosestTo(arr, 7)).toStrictEqual([7, 6, '5', 4, 3, 2, 1]);
expect(sortClosestTo(arr, 4)).toStrictEqual([4, '5', 3, 6, 2, 7, 1]);
expect(sortClosestTo(arr, 2)).toStrictEqual([2, 3, 1, 4, '5', 6, 7]);
expect(sortClosestTo(arr, 6)).toStrictEqual([6, '5', 7, 4, 3, 2, 1]);
});
it('should sort with getter', () => {
const arr = [
1,
2,
{ label: '3', value: '3' },
4,
'5',
6,
{ label: '7', value: 7 },
];
const getter = val => val?.value ?? val;
expect(sortClosestTo(arr, 1, getter)).toStrictEqual([
1,
2,
{ label: '3', value: '3' },
4,
'5',
6,
{ label: '7', value: 7 },
]);
expect(sortClosestTo(arr, 7, getter)).toStrictEqual([
{ label: '7', value: 7 },
6,
'5',
4,
{ label: '3', value: '3' },
2,
1,
]);
expect(sortClosestTo(arr, 4, getter)).toStrictEqual([
4,
'5',
{ label: '3', value: '3' },
6,
2,
{ label: '7', value: 7 },
1,
]);
});
it('should return array filtered by filter fn', () => {
const arr = [1, 2, 3, 4, '5', 6, 7];
const isEven = val => val % 2 === 0;
const isOdd = val => !isEven(val);
expect(sortClosestTo(arr, 0, undefined, isEven)).toStrictEqual([2, 4, 6]);
expect(sortClosestTo(arr, 1, undefined, isOdd)).toStrictEqual([
1,
3,
'5',
7,
]);
});
it('should return result if values empty/invalid', () => {
expect(sortClosestTo()).toStrictEqual([]);
expect(sortClosestTo([])).toStrictEqual([]);
expect(sortClosestTo([], 1)).toStrictEqual([]);
expect(sortClosestTo([7, 1, 2], 'a')).toStrictEqual([]);
expect(sortClosestTo([7, 1, 2], '')).toStrictEqual([]);
expect(sortClosestTo([7, 1, 2], null)).toStrictEqual([]);
expect(sortClosestTo([7, 1, 2], undefined)).toStrictEqual([]);
expect(sortClosestTo([7, 1, 2], {})).toStrictEqual([]);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment