autoComplete.spec.jsx 19 KB

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