autoComplete.spec.jsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. import React from 'react';
  2. import {mount} from 'enzyme';
  3. import AutoComplete from 'app/components/autoComplete';
  4. const items = [
  5. {
  6. name: 'Apple',
  7. },
  8. {
  9. name: 'Pineapple',
  10. },
  11. {
  12. name: 'Orange',
  13. },
  14. ];
  15. /**
  16. * For every render, we push all injected params into `autoCompleteState`, we probably want to
  17. * assert against those instead of the wrapper's state since component state will be different if we have
  18. * "controlled" props where <AutoComplete> does not handle state
  19. */
  20. describe('AutoComplete', function() {
  21. let wrapper;
  22. let input;
  23. let autoCompleteState = [];
  24. let mocks = {
  25. onSelect: jest.fn(),
  26. onClose: jest.fn(),
  27. onOpen: jest.fn(),
  28. };
  29. const createWrapper = props => {
  30. autoCompleteState = [];
  31. Object.keys(mocks).forEach(key => mocks[key].mockReset());
  32. wrapper = mount(
  33. <AutoComplete {...mocks} itemToString={item => item.name} {...props}>
  34. {injectedProps => {
  35. let {
  36. getRootProps,
  37. getInputProps,
  38. getMenuProps,
  39. getItemProps,
  40. inputValue,
  41. highlightedIndex,
  42. isOpen,
  43. } = injectedProps;
  44. // This is purely for testing
  45. autoCompleteState.push(injectedProps);
  46. return (
  47. <div {...getRootProps({style: {position: 'relative'}})}>
  48. <input {...getInputProps({})} />
  49. {isOpen && (
  50. <div
  51. {...getMenuProps({
  52. style: {
  53. boxShadow:
  54. '0 1px 4px 1px rgba(47,40,55,0.08), 0 4px 16px 0 rgba(47,40,55,0.12)',
  55. position: 'absolute',
  56. backgroundColor: 'white',
  57. padding: '0',
  58. },
  59. })}
  60. >
  61. <ul>
  62. {items
  63. .filter(
  64. item =>
  65. item.name.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
  66. )
  67. .map((item, index) => (
  68. <li
  69. key={item.name}
  70. {...getItemProps({
  71. item,
  72. index,
  73. style: {
  74. cursor: 'pointer',
  75. padding: '6px 12px',
  76. backgroundColor:
  77. index === highlightedIndex
  78. ? 'rgba(0, 0, 0, 0.02)'
  79. : undefined,
  80. },
  81. })}
  82. >
  83. {item.name}
  84. </li>
  85. ))}
  86. </ul>
  87. </div>
  88. )}
  89. </div>
  90. );
  91. }}
  92. </AutoComplete>
  93. );
  94. input = wrapper.find('input');
  95. return wrapper;
  96. };
  97. describe('Uncontrolled', function() {
  98. beforeEach(() => {
  99. wrapper = createWrapper();
  100. });
  101. it('shows dropdown menu when input has focus', function() {
  102. input.simulate('focus');
  103. expect(wrapper.state('isOpen')).toBe(true);
  104. expect(wrapper.find('li')).toHaveLength(3);
  105. });
  106. it('only tries to close once if input is blurred and click outside occurs', function() {
  107. jest.useFakeTimers();
  108. input.simulate('focus');
  109. input.simulate('blur');
  110. expect(wrapper.state('isOpen')).toBe(true);
  111. expect(wrapper.find('li')).toHaveLength(3);
  112. wrapper.find('DropdownMenu').prop('onClickOutside')();
  113. jest.runAllTimers();
  114. wrapper.update();
  115. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  116. });
  117. it('only calls onClose dropdown menu when input is blurred', function() {
  118. jest.useFakeTimers();
  119. input.simulate('focus');
  120. input.simulate('blur');
  121. expect(wrapper.state('isOpen')).toBe(true);
  122. expect(wrapper.find('li')).toHaveLength(3);
  123. jest.runAllTimers();
  124. wrapper.update();
  125. expect(wrapper.state('isOpen')).toBe(false);
  126. expect(wrapper.find('li')).toHaveLength(0);
  127. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  128. });
  129. it('can close dropdown menu when Escape is pressed', function() {
  130. input.simulate('focus');
  131. expect(wrapper.state('isOpen')).toBe(true);
  132. input.simulate('keyDown', {key: 'Escape'});
  133. expect(wrapper.state('isOpen')).toBe(false);
  134. });
  135. it('can open and close dropdown menu using injected actions', function() {
  136. let [injectedProps] = autoCompleteState;
  137. injectedProps.actions.open();
  138. expect(wrapper.state('isOpen')).toBe(true);
  139. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  140. injectedProps.actions.close();
  141. expect(wrapper.state('isOpen')).toBe(false);
  142. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  143. });
  144. it('reopens dropdown menu after Escape is pressed and input is changed', function() {
  145. input.simulate('focus');
  146. expect(wrapper.state('isOpen')).toBe(true);
  147. input.simulate('keyDown', {key: 'Escape'});
  148. expect(wrapper.state('isOpen')).toBe(false);
  149. input.simulate('change', {target: {value: 'a'}});
  150. expect(wrapper.state('isOpen')).toBe(true);
  151. expect(wrapper.instance().items.size).toBe(3);
  152. });
  153. it('reopens dropdown menu after item is selected and then input is changed', function() {
  154. input.simulate('focus');
  155. expect(wrapper.state('isOpen')).toBe(true);
  156. input.simulate('change', {target: {value: 'eapp'}});
  157. expect(wrapper.state('isOpen')).toBe(true);
  158. expect(wrapper.instance().items.size).toBe(1);
  159. input.simulate('keyDown', {key: 'Enter'});
  160. expect(wrapper.state('isOpen')).toBe(false);
  161. input.simulate('change', {target: {value: 'app'}});
  162. expect(wrapper.state('isOpen')).toBe(true);
  163. expect(wrapper.instance().items.size).toBe(2);
  164. });
  165. it('selects dropdown item by clicking and sets input to selected value', function() {
  166. input.simulate('focus');
  167. expect(wrapper.state('isOpen')).toBe(true);
  168. expect(wrapper.instance().items.size).toBe(3);
  169. wrapper
  170. .find('li')
  171. .at(1)
  172. .simulate('click');
  173. expect(mocks.onSelect).toHaveBeenCalledWith(
  174. items[1],
  175. expect.objectContaining({inputValue: '', highlightedIndex: 0}),
  176. expect.anything()
  177. );
  178. expect(wrapper.state('inputValue')).toBe('Pineapple');
  179. expect(wrapper.instance().items.size).toBe(0);
  180. });
  181. it('can navigate dropdown items with keyboard and select with "Enter" keypress', function() {
  182. input.simulate('focus');
  183. expect(wrapper.state('isOpen')).toBe(true);
  184. expect(wrapper.state('highlightedIndex')).toBe(0);
  185. input.simulate('keyDown', {key: 'ArrowDown'});
  186. expect(wrapper.state('highlightedIndex')).toBe(1);
  187. input.simulate('keyDown', {key: 'ArrowDown'});
  188. expect(wrapper.state('highlightedIndex')).toBe(2);
  189. expect(wrapper.instance().items.size).toBe(3);
  190. input.simulate('keyDown', {key: 'Enter'});
  191. expect(mocks.onSelect).toHaveBeenCalledWith(
  192. items[2],
  193. expect.objectContaining({inputValue: '', highlightedIndex: 2}),
  194. expect.anything()
  195. );
  196. expect(wrapper.instance().items.size).toBe(0);
  197. expect(wrapper.state('inputValue')).toBe('Orange');
  198. });
  199. it('respects list bounds when navigating filtered items with arrow keys', function() {
  200. input.simulate('focus');
  201. expect(wrapper.state('isOpen')).toBe(true);
  202. expect(wrapper.state('highlightedIndex')).toBe(0);
  203. input.simulate('keyDown', {key: 'ArrowUp'});
  204. expect(wrapper.state('highlightedIndex')).toBe(0);
  205. input.simulate('keyDown', {key: 'ArrowDown'});
  206. expect(wrapper.state('highlightedIndex')).toBe(1);
  207. input.simulate('keyDown', {key: 'ArrowDown'});
  208. expect(wrapper.state('highlightedIndex')).toBe(2);
  209. input.simulate('keyDown', {key: 'ArrowDown'});
  210. expect(wrapper.state('highlightedIndex')).toBe(2);
  211. input.simulate('keyDown', {key: 'ArrowUp'});
  212. expect(wrapper.state('highlightedIndex')).toBe(1);
  213. input.simulate('keyDown', {key: 'ArrowUp'});
  214. expect(wrapper.state('highlightedIndex')).toBe(0);
  215. input.simulate('keyDown', {key: 'ArrowUp'});
  216. expect(wrapper.state('highlightedIndex')).toBe(0);
  217. expect(wrapper.instance().items.size).toBe(3);
  218. });
  219. it('can filter items and then navigate with keyboard', function() {
  220. input.simulate('focus');
  221. expect(wrapper.state('isOpen')).toBe(true);
  222. expect(wrapper.state('highlightedIndex')).toBe(0);
  223. expect(wrapper.instance().items.size).toBe(3);
  224. input.simulate('change', {target: {value: 'a'}});
  225. expect(wrapper.state('highlightedIndex')).toBe(0);
  226. expect(wrapper.state('inputValue')).toBe('a');
  227. // Apple, pineapple, orange
  228. expect(wrapper.instance().items.size).toBe(3);
  229. input.simulate('change', {target: {value: 'ap'}});
  230. expect(wrapper.state('highlightedIndex')).toBe(0);
  231. expect(wrapper.state('inputValue')).toBe('ap');
  232. expect(autoCompleteState[autoCompleteState.length - 1].inputValue).toBe('ap');
  233. // Apple, pineapple
  234. expect(wrapper.instance().items.size).toBe(2);
  235. input.simulate('keyDown', {key: 'ArrowDown'});
  236. expect(wrapper.state('highlightedIndex')).toBe(1);
  237. input.simulate('keyDown', {key: 'ArrowDown'});
  238. expect(wrapper.state('highlightedIndex')).toBe(1);
  239. expect(wrapper.instance().items.size).toBe(2);
  240. input.simulate('keyDown', {key: 'Enter'});
  241. expect(mocks.onSelect).toHaveBeenCalledWith(
  242. items[1],
  243. expect.objectContaining({inputValue: 'ap', highlightedIndex: 1}),
  244. expect.anything()
  245. );
  246. expect(wrapper.instance().items.size).toBe(0);
  247. expect(wrapper.state('inputValue')).toBe('Pineapple');
  248. });
  249. it('can reset input when menu closes', function() {
  250. jest.useFakeTimers();
  251. wrapper.setProps({resetInputOnClose: true});
  252. input.simulate('focus');
  253. expect(wrapper.state('isOpen')).toBe(true);
  254. input.simulate('change', {target: {value: 'a'}});
  255. expect(wrapper.state('inputValue')).toBe('a');
  256. input.simulate('blur');
  257. jest.runAllTimers();
  258. expect(wrapper.state('isOpen')).toBe(false);
  259. expect(wrapper.state('inputValue')).toBe('');
  260. });
  261. });
  262. describe('Controlled', function() {
  263. beforeEach(function() {
  264. wrapper = createWrapper({isOpen: true});
  265. });
  266. it('has dropdown menu initially open', function() {
  267. expect(wrapper.state('isOpen')).toBe(true);
  268. expect(wrapper.find('li')).toHaveLength(3);
  269. });
  270. it('closes when props change', function() {
  271. wrapper.setProps({isOpen: false});
  272. expect(wrapper.state('isOpen')).toBe(true);
  273. wrapper.update();
  274. // Menu should be closed
  275. expect(wrapper.state('isOpen')).toBe(true);
  276. expect(wrapper.find('li')).toHaveLength(0);
  277. });
  278. it('remains closed when input is focused, but calls `onOpen`', function() {
  279. wrapper = createWrapper({isOpen: false});
  280. jest.useFakeTimers();
  281. expect(wrapper.state('isOpen')).toBe(false);
  282. input.simulate('focus');
  283. jest.runAllTimers();
  284. wrapper.update();
  285. expect(wrapper.state('isOpen')).toBe(false);
  286. expect(wrapper.find('li')).toHaveLength(0);
  287. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  288. });
  289. it('remains open when input focus/blur events occur, but calls `onClose`', function() {
  290. jest.useFakeTimers();
  291. input.simulate('focus');
  292. input.simulate('blur');
  293. jest.runAllTimers();
  294. wrapper.update();
  295. expect(wrapper.state('isOpen')).toBe(true);
  296. expect(wrapper.find('li')).toHaveLength(3);
  297. // This still gets called even though menu is open
  298. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  299. });
  300. it('calls onClose when Escape is pressed', function() {
  301. expect(wrapper.state('isOpen')).toBe(true);
  302. input.simulate('keyDown', {key: 'Escape'});
  303. expect(wrapper.state('isOpen')).toBe(true);
  304. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  305. });
  306. it('does not open and close dropdown menu using injected actions', function() {
  307. let [injectedProps] = autoCompleteState;
  308. injectedProps.actions.open();
  309. expect(wrapper.state('isOpen')).toBe(true);
  310. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  311. injectedProps.actions.close();
  312. expect(wrapper.state('isOpen')).toBe(true);
  313. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  314. });
  315. it('onClose is called after item is selected', function() {
  316. expect(wrapper.state('isOpen')).toBe(true);
  317. input.simulate('change', {target: {value: 'eapp'}});
  318. expect(wrapper.state('isOpen')).toBe(true);
  319. expect(wrapper.instance().items.size).toBe(1);
  320. input.simulate('keyDown', {key: 'Enter'});
  321. expect(wrapper.state('isOpen')).toBe(true);
  322. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  323. });
  324. it('selects dropdown item by clicking and sets input to selected value', function() {
  325. expect(wrapper.instance().items.size).toBe(3);
  326. wrapper
  327. .find('li')
  328. .at(1)
  329. .simulate('click');
  330. expect(mocks.onSelect).toHaveBeenCalledWith(
  331. items[1],
  332. expect.objectContaining({inputValue: '', highlightedIndex: 0}),
  333. expect.anything()
  334. );
  335. expect(wrapper.state('inputValue')).toBe('Pineapple');
  336. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  337. });
  338. it('can navigate dropdown items with keyboard and select with "Enter" keypress', function() {
  339. expect(wrapper.state('isOpen')).toBe(true);
  340. expect(wrapper.state('highlightedIndex')).toBe(0);
  341. input.simulate('keyDown', {key: 'ArrowDown'});
  342. expect(wrapper.state('highlightedIndex')).toBe(1);
  343. input.simulate('keyDown', {key: 'ArrowDown'});
  344. expect(wrapper.state('highlightedIndex')).toBe(2);
  345. expect(wrapper.instance().items.size).toBe(3);
  346. input.simulate('keyDown', {key: 'Enter'});
  347. expect(mocks.onSelect).toHaveBeenCalledWith(
  348. items[2],
  349. expect.objectContaining({inputValue: '', highlightedIndex: 2}),
  350. expect.anything()
  351. );
  352. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  353. expect(wrapper.state('inputValue')).toBe('Orange');
  354. });
  355. it('respects list bounds when navigating filtered items with arrow keys', function() {
  356. expect(wrapper.state('isOpen')).toBe(true);
  357. expect(wrapper.state('highlightedIndex')).toBe(0);
  358. input.simulate('keyDown', {key: 'ArrowUp'});
  359. expect(wrapper.state('highlightedIndex')).toBe(0);
  360. input.simulate('keyDown', {key: 'ArrowDown'});
  361. expect(wrapper.state('highlightedIndex')).toBe(1);
  362. input.simulate('keyDown', {key: 'ArrowDown'});
  363. expect(wrapper.state('highlightedIndex')).toBe(2);
  364. input.simulate('keyDown', {key: 'ArrowDown'});
  365. expect(wrapper.state('highlightedIndex')).toBe(2);
  366. input.simulate('keyDown', {key: 'ArrowUp'});
  367. expect(wrapper.state('highlightedIndex')).toBe(1);
  368. input.simulate('keyDown', {key: 'ArrowUp'});
  369. expect(wrapper.state('highlightedIndex')).toBe(0);
  370. input.simulate('keyDown', {key: 'ArrowUp'});
  371. expect(wrapper.state('highlightedIndex')).toBe(0);
  372. expect(wrapper.instance().items.size).toBe(3);
  373. });
  374. it('can filter items and then navigate with keyboard', function() {
  375. expect(wrapper.state('isOpen')).toBe(true);
  376. expect(wrapper.state('highlightedIndex')).toBe(0);
  377. expect(wrapper.instance().items.size).toBe(3);
  378. input.simulate('change', {target: {value: 'a'}});
  379. expect(wrapper.state('highlightedIndex')).toBe(0);
  380. expect(wrapper.state('inputValue')).toBe('a');
  381. // Apple, pineapple, orange
  382. expect(wrapper.instance().items.size).toBe(3);
  383. input.simulate('change', {target: {value: 'ap'}});
  384. expect(wrapper.state('highlightedIndex')).toBe(0);
  385. expect(wrapper.state('inputValue')).toBe('ap');
  386. expect(autoCompleteState[autoCompleteState.length - 1].inputValue).toBe('ap');
  387. // Apple, pineapple
  388. expect(wrapper.instance().items.size).toBe(2);
  389. input.simulate('keyDown', {key: 'ArrowDown'});
  390. expect(wrapper.state('highlightedIndex')).toBe(1);
  391. input.simulate('keyDown', {key: 'ArrowDown'});
  392. expect(wrapper.state('highlightedIndex')).toBe(1);
  393. expect(wrapper.instance().items.size).toBe(2);
  394. input.simulate('keyDown', {key: 'Enter'});
  395. expect(mocks.onSelect).toHaveBeenCalledWith(
  396. items[1],
  397. expect.objectContaining({inputValue: 'ap', highlightedIndex: 1}),
  398. expect.anything()
  399. );
  400. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  401. expect(wrapper.state('inputValue')).toBe('Pineapple');
  402. });
  403. });
  404. it('selects using enter key', function() {
  405. wrapper = createWrapper({isOpen: true, shouldSelectWithEnter: false});
  406. input.simulate('change', {target: {value: 'pine'}});
  407. input.simulate('keyDown', {key: 'Enter'});
  408. expect(mocks.onSelect).not.toHaveBeenCalled();
  409. wrapper = createWrapper({isOpen: true, shouldSelectWithEnter: true});
  410. input.simulate('change', {target: {value: 'pine'}});
  411. input.simulate('keyDown', {key: 'Enter'});
  412. expect(mocks.onSelect).toHaveBeenCalledWith(
  413. items[1],
  414. expect.objectContaining({inputValue: 'pine', highlightedIndex: 0}),
  415. expect.anything()
  416. );
  417. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  418. expect(wrapper.state('inputValue')).toBe('Pineapple');
  419. });
  420. it('selects using tab key', function() {
  421. wrapper = createWrapper({isOpen: true, shouldSelectWithTab: false});
  422. input.simulate('change', {target: {value: 'pine'}});
  423. input.simulate('keyDown', {key: 'Tab'});
  424. expect(mocks.onSelect).not.toHaveBeenCalled();
  425. wrapper = createWrapper({isOpen: true, shouldSelectWithTab: true});
  426. input.simulate('change', {target: {value: 'pine'}});
  427. input.simulate('keyDown', {key: 'Tab'});
  428. expect(mocks.onSelect).toHaveBeenCalledWith(
  429. items[1],
  430. expect.objectContaining({inputValue: 'pine', highlightedIndex: 0}),
  431. expect.anything()
  432. );
  433. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  434. expect(wrapper.state('inputValue')).toBe('Pineapple');
  435. });
  436. it('does not reset highlight state if `closeOnSelect` is false and we select a new item', function() {
  437. wrapper = createWrapper({closeOnSelect: false});
  438. jest.useFakeTimers();
  439. input.simulate('focus');
  440. expect(wrapper.state('isOpen')).toBe(true);
  441. input.simulate('keyDown', {key: 'ArrowDown'});
  442. expect(wrapper.state('highlightedIndex')).toBe(1);
  443. // Select item
  444. input.simulate('keyDown', {key: 'Enter'});
  445. // Should still remain open with same highlightedIndex
  446. expect(wrapper.state('highlightedIndex')).toBe(1);
  447. expect(wrapper.state('isOpen')).toBe(true);
  448. input.simulate('keyDown', {key: 'ArrowDown'});
  449. expect(wrapper.state('highlightedIndex')).toBe(2);
  450. // Select item
  451. input.simulate('keyDown', {key: 'Enter'});
  452. // Should still remain open with same highlightedIndex
  453. expect(wrapper.state('highlightedIndex')).toBe(2);
  454. expect(wrapper.state('isOpen')).toBe(true);
  455. });
  456. });