autoComplete.spec.jsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. import React from 'react';
  2. import {mount} from 'sentry-test/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. const 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. const {
  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', async 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. await Promise.resolve();
  115. wrapper.update();
  116. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  117. });
  118. it('only calls onClose dropdown menu when input is blurred', function() {
  119. jest.useFakeTimers();
  120. input.simulate('focus');
  121. input.simulate('blur');
  122. expect(wrapper.state('isOpen')).toBe(true);
  123. expect(wrapper.find('li')).toHaveLength(3);
  124. jest.runAllTimers();
  125. wrapper.update();
  126. expect(wrapper.state('isOpen')).toBe(false);
  127. expect(wrapper.find('li')).toHaveLength(0);
  128. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  129. });
  130. it('can close dropdown menu when Escape is pressed', function() {
  131. input.simulate('focus');
  132. expect(wrapper.state('isOpen')).toBe(true);
  133. input.simulate('keyDown', {key: 'Escape'});
  134. expect(wrapper.state('isOpen')).toBe(false);
  135. });
  136. it('can open and close dropdown menu using injected actions', function() {
  137. const [injectedProps] = autoCompleteState;
  138. injectedProps.actions.open();
  139. expect(wrapper.state('isOpen')).toBe(true);
  140. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  141. injectedProps.actions.close();
  142. expect(wrapper.state('isOpen')).toBe(false);
  143. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  144. });
  145. it('reopens dropdown menu after Escape is pressed and input is changed', function() {
  146. input.simulate('focus');
  147. expect(wrapper.state('isOpen')).toBe(true);
  148. input.simulate('keyDown', {key: 'Escape'});
  149. expect(wrapper.state('isOpen')).toBe(false);
  150. input.simulate('change', {target: {value: 'a'}});
  151. expect(wrapper.state('isOpen')).toBe(true);
  152. expect(wrapper.instance().items.size).toBe(3);
  153. });
  154. it('reopens dropdown menu after item is selected and then input is changed', function() {
  155. input.simulate('focus');
  156. expect(wrapper.state('isOpen')).toBe(true);
  157. input.simulate('change', {target: {value: 'eapp'}});
  158. expect(wrapper.state('isOpen')).toBe(true);
  159. expect(wrapper.instance().items.size).toBe(1);
  160. input.simulate('keyDown', {key: 'Enter'});
  161. expect(wrapper.state('isOpen')).toBe(false);
  162. input.simulate('change', {target: {value: 'app'}});
  163. expect(wrapper.state('isOpen')).toBe(true);
  164. expect(wrapper.instance().items.size).toBe(2);
  165. });
  166. it('selects dropdown item by clicking and sets input to selected value', function() {
  167. input.simulate('focus');
  168. expect(wrapper.state('isOpen')).toBe(true);
  169. expect(wrapper.instance().items.size).toBe(3);
  170. wrapper
  171. .find('li')
  172. .at(1)
  173. .simulate('click');
  174. expect(mocks.onSelect).toHaveBeenCalledWith(
  175. items[1],
  176. expect.objectContaining({inputValue: '', highlightedIndex: 0}),
  177. expect.anything()
  178. );
  179. expect(wrapper.state('inputValue')).toBe('Pineapple');
  180. expect(wrapper.instance().items.size).toBe(0);
  181. });
  182. it('can navigate dropdown items with keyboard and select with "Enter" keypress', function() {
  183. input.simulate('focus');
  184. expect(wrapper.state('isOpen')).toBe(true);
  185. expect(wrapper.state('highlightedIndex')).toBe(0);
  186. input.simulate('keyDown', {key: 'ArrowDown'});
  187. expect(wrapper.state('highlightedIndex')).toBe(1);
  188. input.simulate('keyDown', {key: 'ArrowDown'});
  189. expect(wrapper.state('highlightedIndex')).toBe(2);
  190. expect(wrapper.instance().items.size).toBe(3);
  191. input.simulate('keyDown', {key: 'Enter'});
  192. expect(mocks.onSelect).toHaveBeenCalledWith(
  193. items[2],
  194. expect.objectContaining({inputValue: '', highlightedIndex: 2}),
  195. expect.anything()
  196. );
  197. expect(wrapper.instance().items.size).toBe(0);
  198. expect(wrapper.state('inputValue')).toBe('Orange');
  199. });
  200. it('respects list bounds when navigating filtered items with arrow keys', function() {
  201. input.simulate('focus');
  202. expect(wrapper.state('isOpen')).toBe(true);
  203. expect(wrapper.state('highlightedIndex')).toBe(0);
  204. input.simulate('keyDown', {key: 'ArrowUp'});
  205. expect(wrapper.state('highlightedIndex')).toBe(0);
  206. input.simulate('keyDown', {key: 'ArrowDown'});
  207. expect(wrapper.state('highlightedIndex')).toBe(1);
  208. input.simulate('keyDown', {key: 'ArrowDown'});
  209. expect(wrapper.state('highlightedIndex')).toBe(2);
  210. input.simulate('keyDown', {key: 'ArrowDown'});
  211. expect(wrapper.state('highlightedIndex')).toBe(2);
  212. input.simulate('keyDown', {key: 'ArrowUp'});
  213. expect(wrapper.state('highlightedIndex')).toBe(1);
  214. input.simulate('keyDown', {key: 'ArrowUp'});
  215. expect(wrapper.state('highlightedIndex')).toBe(0);
  216. input.simulate('keyDown', {key: 'ArrowUp'});
  217. expect(wrapper.state('highlightedIndex')).toBe(0);
  218. expect(wrapper.instance().items.size).toBe(3);
  219. });
  220. it('can filter items and then navigate with keyboard', function() {
  221. input.simulate('focus');
  222. expect(wrapper.state('isOpen')).toBe(true);
  223. expect(wrapper.state('highlightedIndex')).toBe(0);
  224. expect(wrapper.instance().items.size).toBe(3);
  225. input.simulate('change', {target: {value: 'a'}});
  226. expect(wrapper.state('highlightedIndex')).toBe(0);
  227. expect(wrapper.state('inputValue')).toBe('a');
  228. // Apple, pineapple, orange
  229. expect(wrapper.instance().items.size).toBe(3);
  230. input.simulate('change', {target: {value: 'ap'}});
  231. expect(wrapper.state('highlightedIndex')).toBe(0);
  232. expect(wrapper.state('inputValue')).toBe('ap');
  233. expect(autoCompleteState[autoCompleteState.length - 1].inputValue).toBe('ap');
  234. // Apple, pineapple
  235. expect(wrapper.instance().items.size).toBe(2);
  236. input.simulate('keyDown', {key: 'ArrowDown'});
  237. expect(wrapper.state('highlightedIndex')).toBe(1);
  238. input.simulate('keyDown', {key: 'ArrowDown'});
  239. expect(wrapper.state('highlightedIndex')).toBe(1);
  240. expect(wrapper.instance().items.size).toBe(2);
  241. input.simulate('keyDown', {key: 'Enter'});
  242. expect(mocks.onSelect).toHaveBeenCalledWith(
  243. items[1],
  244. expect.objectContaining({inputValue: 'ap', highlightedIndex: 1}),
  245. expect.anything()
  246. );
  247. expect(wrapper.instance().items.size).toBe(0);
  248. expect(wrapper.state('inputValue')).toBe('Pineapple');
  249. });
  250. it('can reset input when menu closes', function() {
  251. jest.useFakeTimers();
  252. wrapper.setProps({resetInputOnClose: true});
  253. input.simulate('focus');
  254. expect(wrapper.state('isOpen')).toBe(true);
  255. input.simulate('change', {target: {value: 'a'}});
  256. expect(wrapper.state('inputValue')).toBe('a');
  257. input.simulate('blur');
  258. jest.runAllTimers();
  259. expect(wrapper.state('isOpen')).toBe(false);
  260. expect(wrapper.state('inputValue')).toBe('');
  261. });
  262. });
  263. describe('Controlled', function() {
  264. beforeEach(function() {
  265. wrapper = createWrapper({isOpen: true});
  266. });
  267. it('has dropdown menu initially open', function() {
  268. expect(wrapper.state('isOpen')).toBe(true);
  269. expect(wrapper.find('li')).toHaveLength(3);
  270. });
  271. it('closes when props change', function() {
  272. wrapper.setProps({isOpen: false});
  273. expect(wrapper.state('isOpen')).toBe(true);
  274. wrapper.update();
  275. // Menu should be closed
  276. expect(wrapper.state('isOpen')).toBe(true);
  277. expect(wrapper.find('li')).toHaveLength(0);
  278. });
  279. it('remains closed when input is focused, but calls `onOpen`', function() {
  280. wrapper = createWrapper({isOpen: false});
  281. jest.useFakeTimers();
  282. expect(wrapper.state('isOpen')).toBe(false);
  283. input.simulate('focus');
  284. jest.runAllTimers();
  285. wrapper.update();
  286. expect(wrapper.state('isOpen')).toBe(false);
  287. expect(wrapper.find('li')).toHaveLength(0);
  288. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  289. });
  290. it('remains open when input focus/blur events occur, but calls `onClose`', function() {
  291. jest.useFakeTimers();
  292. input.simulate('focus');
  293. input.simulate('blur');
  294. jest.runAllTimers();
  295. wrapper.update();
  296. expect(wrapper.state('isOpen')).toBe(true);
  297. expect(wrapper.find('li')).toHaveLength(3);
  298. // This still gets called even though menu is open
  299. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  300. });
  301. it('calls onClose when Escape is pressed', function() {
  302. expect(wrapper.state('isOpen')).toBe(true);
  303. input.simulate('keyDown', {key: 'Escape'});
  304. expect(wrapper.state('isOpen')).toBe(true);
  305. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  306. });
  307. it('does not open and close dropdown menu using injected actions', function() {
  308. const [injectedProps] = autoCompleteState;
  309. injectedProps.actions.open();
  310. expect(wrapper.state('isOpen')).toBe(true);
  311. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  312. injectedProps.actions.close();
  313. expect(wrapper.state('isOpen')).toBe(true);
  314. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  315. });
  316. it('onClose is called after item is selected', function() {
  317. expect(wrapper.state('isOpen')).toBe(true);
  318. input.simulate('change', {target: {value: 'eapp'}});
  319. expect(wrapper.state('isOpen')).toBe(true);
  320. expect(wrapper.instance().items.size).toBe(1);
  321. input.simulate('keyDown', {key: 'Enter'});
  322. expect(wrapper.state('isOpen')).toBe(true);
  323. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  324. });
  325. it('selects dropdown item by clicking and sets input to selected value', function() {
  326. expect(wrapper.instance().items.size).toBe(3);
  327. wrapper
  328. .find('li')
  329. .at(1)
  330. .simulate('click');
  331. expect(mocks.onSelect).toHaveBeenCalledWith(
  332. items[1],
  333. expect.objectContaining({inputValue: '', highlightedIndex: 0}),
  334. expect.anything()
  335. );
  336. expect(wrapper.state('inputValue')).toBe('Pineapple');
  337. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  338. });
  339. it('can navigate dropdown items with keyboard and select with "Enter" keypress', function() {
  340. expect(wrapper.state('isOpen')).toBe(true);
  341. expect(wrapper.state('highlightedIndex')).toBe(0);
  342. input.simulate('keyDown', {key: 'ArrowDown'});
  343. expect(wrapper.state('highlightedIndex')).toBe(1);
  344. input.simulate('keyDown', {key: 'ArrowDown'});
  345. expect(wrapper.state('highlightedIndex')).toBe(2);
  346. expect(wrapper.instance().items.size).toBe(3);
  347. input.simulate('keyDown', {key: 'Enter'});
  348. expect(mocks.onSelect).toHaveBeenCalledWith(
  349. items[2],
  350. expect.objectContaining({inputValue: '', highlightedIndex: 2}),
  351. expect.anything()
  352. );
  353. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  354. expect(wrapper.state('inputValue')).toBe('Orange');
  355. });
  356. it('respects list bounds when navigating filtered items with arrow keys', function() {
  357. expect(wrapper.state('isOpen')).toBe(true);
  358. expect(wrapper.state('highlightedIndex')).toBe(0);
  359. input.simulate('keyDown', {key: 'ArrowUp'});
  360. expect(wrapper.state('highlightedIndex')).toBe(0);
  361. input.simulate('keyDown', {key: 'ArrowDown'});
  362. expect(wrapper.state('highlightedIndex')).toBe(1);
  363. input.simulate('keyDown', {key: 'ArrowDown'});
  364. expect(wrapper.state('highlightedIndex')).toBe(2);
  365. input.simulate('keyDown', {key: 'ArrowDown'});
  366. expect(wrapper.state('highlightedIndex')).toBe(2);
  367. input.simulate('keyDown', {key: 'ArrowUp'});
  368. expect(wrapper.state('highlightedIndex')).toBe(1);
  369. input.simulate('keyDown', {key: 'ArrowUp'});
  370. expect(wrapper.state('highlightedIndex')).toBe(0);
  371. input.simulate('keyDown', {key: 'ArrowUp'});
  372. expect(wrapper.state('highlightedIndex')).toBe(0);
  373. expect(wrapper.instance().items.size).toBe(3);
  374. });
  375. it('can filter items and then navigate with keyboard', function() {
  376. expect(wrapper.state('isOpen')).toBe(true);
  377. expect(wrapper.state('highlightedIndex')).toBe(0);
  378. expect(wrapper.instance().items.size).toBe(3);
  379. input.simulate('change', {target: {value: 'a'}});
  380. expect(wrapper.state('highlightedIndex')).toBe(0);
  381. expect(wrapper.state('inputValue')).toBe('a');
  382. // Apple, pineapple, orange
  383. expect(wrapper.instance().items.size).toBe(3);
  384. input.simulate('change', {target: {value: 'ap'}});
  385. expect(wrapper.state('highlightedIndex')).toBe(0);
  386. expect(wrapper.state('inputValue')).toBe('ap');
  387. expect(autoCompleteState[autoCompleteState.length - 1].inputValue).toBe('ap');
  388. // Apple, pineapple
  389. expect(wrapper.instance().items.size).toBe(2);
  390. input.simulate('keyDown', {key: 'ArrowDown'});
  391. expect(wrapper.state('highlightedIndex')).toBe(1);
  392. input.simulate('keyDown', {key: 'ArrowDown'});
  393. expect(wrapper.state('highlightedIndex')).toBe(1);
  394. expect(wrapper.instance().items.size).toBe(2);
  395. input.simulate('keyDown', {key: 'Enter'});
  396. expect(mocks.onSelect).toHaveBeenCalledWith(
  397. items[1],
  398. expect.objectContaining({inputValue: 'ap', highlightedIndex: 1}),
  399. expect.anything()
  400. );
  401. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  402. expect(wrapper.state('inputValue')).toBe('Pineapple');
  403. });
  404. });
  405. it('selects using enter key', function() {
  406. wrapper = createWrapper({isOpen: true, shouldSelectWithEnter: false});
  407. input.simulate('change', {target: {value: 'pine'}});
  408. input.simulate('keyDown', {key: 'Enter'});
  409. expect(mocks.onSelect).not.toHaveBeenCalled();
  410. wrapper = createWrapper({isOpen: true, shouldSelectWithEnter: true});
  411. input.simulate('change', {target: {value: 'pine'}});
  412. input.simulate('keyDown', {key: 'Enter'});
  413. expect(mocks.onSelect).toHaveBeenCalledWith(
  414. items[1],
  415. expect.objectContaining({inputValue: 'pine', highlightedIndex: 0}),
  416. expect.anything()
  417. );
  418. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  419. expect(wrapper.state('inputValue')).toBe('Pineapple');
  420. });
  421. it('selects using tab key', function() {
  422. wrapper = createWrapper({isOpen: true, shouldSelectWithTab: false});
  423. input.simulate('change', {target: {value: 'pine'}});
  424. input.simulate('keyDown', {key: 'Tab'});
  425. expect(mocks.onSelect).not.toHaveBeenCalled();
  426. wrapper = createWrapper({isOpen: true, shouldSelectWithTab: true});
  427. input.simulate('change', {target: {value: 'pine'}});
  428. input.simulate('keyDown', {key: 'Tab'});
  429. expect(mocks.onSelect).toHaveBeenCalledWith(
  430. items[1],
  431. expect.objectContaining({inputValue: 'pine', highlightedIndex: 0}),
  432. expect.anything()
  433. );
  434. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  435. expect(wrapper.state('inputValue')).toBe('Pineapple');
  436. });
  437. it('does not reset highlight state if `closeOnSelect` is false and we select a new item', function() {
  438. wrapper = createWrapper({closeOnSelect: false});
  439. jest.useFakeTimers();
  440. input.simulate('focus');
  441. expect(wrapper.state('isOpen')).toBe(true);
  442. input.simulate('keyDown', {key: 'ArrowDown'});
  443. expect(wrapper.state('highlightedIndex')).toBe(1);
  444. // Select item
  445. input.simulate('keyDown', {key: 'Enter'});
  446. // Should still remain open with same highlightedIndex
  447. expect(wrapper.state('highlightedIndex')).toBe(1);
  448. expect(wrapper.state('isOpen')).toBe(true);
  449. input.simulate('keyDown', {key: 'ArrowDown'});
  450. expect(wrapper.state('highlightedIndex')).toBe(2);
  451. // Select item
  452. input.simulate('keyDown', {key: 'Enter'});
  453. // Should still remain open with same highlightedIndex
  454. expect(wrapper.state('highlightedIndex')).toBe(2);
  455. expect(wrapper.state('isOpen')).toBe(true);
  456. });
  457. });