autoComplete.spec.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. import {useEffect} from 'react';
  2. import {act, fireEvent, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import type {AutoCompleteProps} from 'sentry/components/autoComplete';
  4. import AutoComplete from 'sentry/components/autoComplete';
  5. const items = [
  6. {
  7. name: 'Apple',
  8. },
  9. {
  10. name: 'Pineapple',
  11. },
  12. {
  13. name: 'Orange',
  14. },
  15. ];
  16. /**
  17. * For every render, we push all injected params into `autoCompleteState`, we probably want to
  18. * assert against those instead of the wrapper's state since component state will be different if we have
  19. * "controlled" props where <AutoComplete> does not handle state
  20. */
  21. describe('AutoComplete', function () {
  22. let input: HTMLInputElement;
  23. let autoCompleteState: any[] = [];
  24. const mocks = {
  25. onSelect: jest.fn(),
  26. onClose: jest.fn(),
  27. onOpen: jest.fn(),
  28. };
  29. afterEach(() => {
  30. jest.resetAllMocks();
  31. autoCompleteState = [];
  32. });
  33. function List({
  34. registerItemCount,
  35. itemCount,
  36. ...props
  37. }: {
  38. children: React.ReactNode;
  39. itemCount: number;
  40. registerItemCount: (count?: number) => void;
  41. }) {
  42. useEffect(() => void registerItemCount(itemCount), [itemCount, registerItemCount]);
  43. return <ul {...props} />;
  44. }
  45. function Item({
  46. registerVisibleItem,
  47. item,
  48. index,
  49. ...props
  50. }: {
  51. children: React.ReactNode;
  52. index: number;
  53. item: {name?: string};
  54. registerVisibleItem: (index: number, item: any) => () => void;
  55. }) {
  56. useEffect(() => registerVisibleItem(index, item), [registerVisibleItem, index, item]);
  57. return <li {...props} />;
  58. }
  59. const createComponent = (props: Partial<AutoCompleteProps<any>>) => (
  60. <AutoComplete {...mocks} itemToString={item => item.name} {...props}>
  61. {injectedProps => {
  62. const {
  63. getRootProps,
  64. getInputProps,
  65. getMenuProps,
  66. getItemProps,
  67. inputValue,
  68. isOpen,
  69. registerItemCount,
  70. registerVisibleItem,
  71. highlightedIndex,
  72. } = injectedProps;
  73. // This is purely for testing
  74. autoCompleteState.push(injectedProps);
  75. const filteredItems = items.filter(item =>
  76. item.name.toLowerCase().includes(inputValue.toLowerCase())
  77. );
  78. return (
  79. <div {...getRootProps()}>
  80. <input placeholder="autocomplete" {...getInputProps({})} />
  81. {isOpen && (
  82. <div {...getMenuProps()} data-test-id="test-autocomplete">
  83. <List
  84. registerItemCount={registerItemCount}
  85. itemCount={filteredItems.length}
  86. >
  87. {filteredItems.map((item, index) => (
  88. <Item
  89. key={item.name}
  90. registerVisibleItem={registerVisibleItem}
  91. index={index}
  92. item={item}
  93. aria-selected={highlightedIndex === index}
  94. {...getItemProps({item, index})}
  95. >
  96. {item.name}
  97. </Item>
  98. ))}
  99. </List>
  100. </div>
  101. )}
  102. </div>
  103. );
  104. }}
  105. </AutoComplete>
  106. );
  107. const createWrapper = (props?: any) => {
  108. const wrapper = render(createComponent(props));
  109. input = screen.getByPlaceholderText('autocomplete');
  110. return wrapper;
  111. };
  112. describe('Uncontrolled', function () {
  113. it('shows dropdown menu when input has focus', function () {
  114. createWrapper();
  115. fireEvent.focus(input);
  116. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  117. for (const item of items) {
  118. expect(screen.getByText(item.name)).toBeInTheDocument();
  119. }
  120. });
  121. it('only tries to close once if input is blurred and click outside occurs', async function () {
  122. createWrapper();
  123. jest.useFakeTimers();
  124. fireEvent.focus(input);
  125. fireEvent.blur(input);
  126. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  127. // Click outside dropdown
  128. fireEvent.click(document.body);
  129. jest.runAllTimers();
  130. await waitFor(() => expect(mocks.onClose).toHaveBeenCalledTimes(1));
  131. });
  132. it('only calls onClose dropdown menu when input is blurred', function () {
  133. createWrapper();
  134. jest.useFakeTimers();
  135. fireEvent.focus(input);
  136. fireEvent.blur(input);
  137. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  138. act(() => jest.runAllTimers());
  139. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  140. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  141. });
  142. it('can close dropdown menu when Escape is pressed', function () {
  143. createWrapper();
  144. fireEvent.focus(input);
  145. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  146. fireEvent.keyDown(input, {key: 'Escape', charCode: 27});
  147. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  148. });
  149. it('can open and close dropdown menu using injected actions', function () {
  150. createWrapper();
  151. const [injectedProps] = autoCompleteState;
  152. act(() => injectedProps.actions.open());
  153. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  154. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  155. act(() => injectedProps.actions.close());
  156. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  157. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  158. });
  159. it('reopens dropdown menu after Escape is pressed and input is changed', function () {
  160. createWrapper();
  161. fireEvent.focus(input);
  162. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  163. fireEvent.keyDown(input, {key: 'Escape', charCode: 27});
  164. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  165. fireEvent.change(input, {target: {value: 'a', charCode: 65}});
  166. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  167. expect(screen.getAllByRole('option')).toHaveLength(3);
  168. });
  169. it('reopens dropdown menu after item is selected and then input is changed', function () {
  170. createWrapper();
  171. fireEvent.focus(input);
  172. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  173. fireEvent.change(input, {target: {value: 'eapp'}});
  174. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  175. expect(screen.getByRole('option')).toBeInTheDocument();
  176. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  177. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  178. fireEvent.change(input, {target: {value: 'app'}});
  179. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  180. expect(screen.getAllByRole('option')).toHaveLength(2);
  181. });
  182. it('selects dropdown item by clicking and sets input to selected value', function () {
  183. createWrapper();
  184. fireEvent.focus(input);
  185. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  186. expect(screen.getAllByRole('option')).toHaveLength(3);
  187. fireEvent.click(screen.getByText(items[1].name));
  188. expect(mocks.onSelect).toHaveBeenCalledWith(
  189. items[1],
  190. expect.objectContaining({inputValue: '', highlightedIndex: 0}),
  191. expect.anything()
  192. );
  193. expect(input).toHaveValue('Pineapple');
  194. expect(screen.queryByRole('option')).not.toBeInTheDocument();
  195. });
  196. it('can navigate dropdown items with keyboard and select with "Enter" keypress', function () {
  197. createWrapper();
  198. fireEvent.focus(input);
  199. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  200. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  201. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  202. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  203. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  204. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  205. expect(screen.getAllByRole('option')).toHaveLength(3);
  206. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  207. expect(mocks.onSelect).toHaveBeenCalledWith(
  208. items[2],
  209. expect.objectContaining({inputValue: '', highlightedIndex: 2}),
  210. expect.anything()
  211. );
  212. expect(screen.queryByRole('option')).not.toBeInTheDocument();
  213. expect(input).toHaveValue('Orange');
  214. });
  215. it('respects list bounds when navigating filtered items with arrow keys', function () {
  216. createWrapper();
  217. fireEvent.focus(input);
  218. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  219. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  220. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  221. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  222. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  223. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  224. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  225. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  226. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  227. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  228. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  229. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  230. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  231. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  232. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  233. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  234. expect(screen.getAllByRole('option')).toHaveLength(3);
  235. });
  236. it('can filter items and then navigate with keyboard', function () {
  237. createWrapper();
  238. fireEvent.focus(input);
  239. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  240. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  241. expect(screen.getAllByRole('option')).toHaveLength(3);
  242. fireEvent.keyDown(input, {target: {value: 'a', charCode: 65}});
  243. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  244. expect(input).toHaveValue('a');
  245. // Apple, pineapple, orange
  246. expect(screen.getAllByRole('option')).toHaveLength(3);
  247. fireEvent.change(input, {target: {value: 'ap'}});
  248. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  249. expect(input).toHaveValue('ap');
  250. expect(autoCompleteState[autoCompleteState.length - 1].inputValue).toBe('ap');
  251. // Apple, pineapple
  252. expect(screen.getAllByRole('option')).toHaveLength(2);
  253. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  254. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  255. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  256. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  257. expect(screen.getAllByRole('option')).toHaveLength(2);
  258. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  259. expect(mocks.onSelect).toHaveBeenCalledWith(
  260. items[1],
  261. expect.objectContaining({inputValue: 'ap', highlightedIndex: 1}),
  262. expect.anything()
  263. );
  264. expect(screen.queryByRole('option')).not.toBeInTheDocument();
  265. expect(input).toHaveValue('Pineapple');
  266. });
  267. it('can reset input when menu closes', function () {
  268. const wrapper = createWrapper();
  269. jest.useFakeTimers();
  270. wrapper.rerender(createComponent({resetInputOnClose: true}));
  271. fireEvent.focus(input);
  272. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  273. fireEvent.keyDown(input, {target: {value: 'a', charCode: 65}});
  274. expect(input).toHaveValue('a');
  275. fireEvent.blur(input);
  276. act(() => jest.runAllTimers());
  277. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  278. expect(input).toHaveValue('');
  279. });
  280. });
  281. describe('isOpen controlled', function () {
  282. it('has dropdown menu initially open', function () {
  283. createWrapper({isOpen: true});
  284. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  285. expect(screen.getAllByRole('option')).toHaveLength(3);
  286. });
  287. it('closes when props change', function () {
  288. const wrapper = createWrapper({isOpen: true});
  289. wrapper.rerender(createComponent({isOpen: false}));
  290. // Menu should be closed
  291. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  292. expect(screen.queryByRole('option')).not.toBeInTheDocument();
  293. });
  294. it('remains closed when input is focused, but calls `onOpen`', function () {
  295. createWrapper({isOpen: false});
  296. jest.useFakeTimers();
  297. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  298. fireEvent.focus(input);
  299. jest.runAllTimers();
  300. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  301. expect(screen.queryByRole('option')).not.toBeInTheDocument();
  302. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  303. });
  304. it('remains open when input focus/blur events occur, but calls `onClose`', function () {
  305. createWrapper({isOpen: true});
  306. jest.useFakeTimers();
  307. fireEvent.focus(input);
  308. fireEvent.blur(input);
  309. act(() => jest.runAllTimers());
  310. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  311. // This still gets called even though menu is open
  312. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  313. });
  314. it('calls onClose when Escape is pressed', function () {
  315. createWrapper({isOpen: true});
  316. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  317. fireEvent.keyDown(input, {key: 'Escape', charCode: 27});
  318. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  319. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  320. });
  321. it('does not open and close dropdown menu using injected actions', function () {
  322. createWrapper({isOpen: true});
  323. const [injectedProps] = autoCompleteState;
  324. act(() => injectedProps.actions.open());
  325. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  326. expect(mocks.onOpen).toHaveBeenCalledTimes(1);
  327. act(() => injectedProps.actions.close());
  328. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  329. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  330. });
  331. it('onClose is called after item is selected', function () {
  332. createWrapper({isOpen: true});
  333. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  334. fireEvent.change(input, {target: {value: 'eapp'}});
  335. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  336. expect(screen.getByRole('option')).toBeInTheDocument();
  337. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  338. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  339. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  340. });
  341. it('selects dropdown item by clicking and sets input to selected value', function () {
  342. createWrapper({isOpen: true});
  343. expect(screen.getAllByRole('option')).toHaveLength(3);
  344. fireEvent.click(screen.getByText(items[1].name));
  345. expect(mocks.onSelect).toHaveBeenCalledWith(
  346. items[1],
  347. expect.objectContaining({inputValue: '', highlightedIndex: 0}),
  348. expect.anything()
  349. );
  350. expect(input).toHaveValue('Pineapple');
  351. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  352. });
  353. it('can navigate dropdown items with keyboard and select with "Enter" keypress', function () {
  354. createWrapper({isOpen: true});
  355. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  356. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  357. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  358. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  359. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  360. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  361. expect(screen.getAllByRole('option')).toHaveLength(3);
  362. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  363. expect(mocks.onSelect).toHaveBeenCalledWith(
  364. items[2],
  365. expect.objectContaining({inputValue: '', highlightedIndex: 2}),
  366. expect.anything()
  367. );
  368. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  369. expect(input).toHaveValue('Orange');
  370. });
  371. it('respects list bounds when navigating filtered items with arrow keys', function () {
  372. createWrapper({isOpen: true});
  373. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  374. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  375. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  376. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  377. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  378. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  379. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  380. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  381. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  382. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  383. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  384. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  385. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  386. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  387. fireEvent.keyDown(input, {key: 'ArrowUp', charCode: 38});
  388. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  389. expect(screen.getAllByRole('option')).toHaveLength(3);
  390. });
  391. it('can filter items and then navigate with keyboard', function () {
  392. createWrapper({isOpen: true});
  393. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  394. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  395. expect(screen.getAllByRole('option')).toHaveLength(3);
  396. fireEvent.keyDown(input, {target: {value: 'a', charCode: 65}});
  397. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  398. expect(input).toHaveValue('a');
  399. // Apple, pineapple, orange
  400. expect(screen.getAllByRole('option')).toHaveLength(3);
  401. fireEvent.change(input, {target: {value: 'ap'}});
  402. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  403. expect(input).toHaveValue('ap');
  404. expect(autoCompleteState[autoCompleteState.length - 1].inputValue).toBe('ap');
  405. // Apple, pineapple
  406. expect(screen.getAllByRole('option')).toHaveLength(2);
  407. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  408. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  409. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  410. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  411. expect(screen.getAllByRole('option')).toHaveLength(2);
  412. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  413. expect(mocks.onSelect).toHaveBeenCalledWith(
  414. items[1],
  415. expect.objectContaining({inputValue: 'ap', highlightedIndex: 1}),
  416. expect.anything()
  417. );
  418. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  419. expect(input).toHaveValue('Pineapple');
  420. });
  421. it('only scrolls highlighted item into view on keyboard events', function () {
  422. const scrollIntoViewMock = jest.fn();
  423. Element.prototype.scrollIntoView = scrollIntoViewMock;
  424. createWrapper({isOpen: true});
  425. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  426. fireEvent.mouseEnter(screen.getByText('Pineapple'));
  427. expect(scrollIntoViewMock).not.toHaveBeenCalled();
  428. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  429. expect(scrollIntoViewMock).toHaveBeenCalledTimes(1);
  430. });
  431. it('can reset input value when menu closes', function () {
  432. const wrapper = createWrapper({isOpen: true});
  433. jest.useFakeTimers();
  434. wrapper.rerender(createComponent({resetInputOnClose: true}));
  435. fireEvent.focus(input);
  436. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  437. fireEvent.keyDown(input, {target: {value: 'a', charCode: 65}});
  438. expect(input).toHaveValue('a');
  439. fireEvent.blur(input);
  440. act(() => jest.runAllTimers());
  441. expect(input).toHaveValue('');
  442. });
  443. });
  444. describe('inputValue controlled', () => {
  445. it('follows the inputValue prop', () => {
  446. const wrapper = createWrapper({inputValue: 'initial value'});
  447. expect(input).toHaveValue('initial value');
  448. wrapper.rerender(createComponent({inputValue: 'new value'}));
  449. expect(input).toHaveValue('new value');
  450. });
  451. it('calls onInputValueChange on input', () => {
  452. const wrapper = createWrapper({inputValue: 'initial value'});
  453. const onInputValueChange = jest.fn();
  454. wrapper.rerender(createComponent({onInputValueChange}));
  455. fireEvent.focus(input);
  456. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  457. fireEvent.change(input, {target: {value: 'a'}});
  458. expect(onInputValueChange).toHaveBeenCalledWith('a');
  459. });
  460. it('input value does not change when typed into', () => {
  461. createWrapper({inputValue: 'initial value'});
  462. fireEvent.focus(input);
  463. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  464. fireEvent.change(input, {target: {value: 'a'}});
  465. expect(input).toHaveValue('initial value');
  466. });
  467. it('input value does not change when blurred', () => {
  468. createWrapper({inputValue: 'initial value'});
  469. fireEvent.focus(input);
  470. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  471. fireEvent.blur(input);
  472. expect(input).toHaveValue('initial value');
  473. });
  474. it('can search for and select an item without changing the input value', () => {
  475. const wrapper = createWrapper({inputValue: 'initial value'});
  476. fireEvent.focus(input);
  477. wrapper.rerender(createComponent({inputValue: 'apple'}));
  478. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  479. expect(screen.getAllByRole('option')).toHaveLength(2);
  480. fireEvent.click(screen.getByText('Apple'));
  481. expect(screen.queryByTestId('test-autocomplete')).not.toBeInTheDocument();
  482. expect(input).toHaveValue('apple');
  483. expect(mocks.onSelect).toHaveBeenCalledWith(
  484. {name: 'Apple'},
  485. expect.anything(),
  486. expect.anything()
  487. );
  488. });
  489. it('can filter and navigate dropdown items with keyboard and select with "Enter" keypress without changing input value', function () {
  490. const wrapper = createWrapper({inputValue: 'initial value'});
  491. wrapper.rerender(createComponent({inputValue: 'apple'}));
  492. fireEvent.focus(input);
  493. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  494. expect(screen.getByText('Apple')).toHaveAttribute('aria-selected', 'true');
  495. expect(screen.getAllByRole('option')).toHaveLength(2);
  496. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  497. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  498. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  499. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  500. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  501. expect(mocks.onSelect).toHaveBeenCalledWith(
  502. items[1],
  503. expect.objectContaining({highlightedIndex: 1}),
  504. expect.anything()
  505. );
  506. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  507. expect(input).toHaveValue('apple');
  508. });
  509. });
  510. it('selects using enter key', function () {
  511. const wrapper = createWrapper({isOpen: true, shouldSelectWithEnter: false});
  512. fireEvent.change(input, {target: {value: 'pine'}});
  513. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  514. expect(mocks.onSelect).not.toHaveBeenCalled();
  515. wrapper.unmount();
  516. createWrapper({isOpen: true, shouldSelectWithEnter: true});
  517. fireEvent.change(input, {target: {value: 'pine'}});
  518. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  519. expect(mocks.onSelect).toHaveBeenCalledWith(
  520. items[1],
  521. expect.objectContaining({inputValue: 'pine', highlightedIndex: 0}),
  522. expect.anything()
  523. );
  524. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  525. expect(input).toHaveValue('Pineapple');
  526. });
  527. it('selects using tab key', function () {
  528. let wrapper = createWrapper({isOpen: true, shouldSelectWithTab: false});
  529. fireEvent.change(input, {target: {value: 'pine'}});
  530. fireEvent.keyDown(input, {key: 'Tab', charCode: 9});
  531. expect(mocks.onSelect).not.toHaveBeenCalled();
  532. wrapper.unmount();
  533. wrapper = createWrapper({isOpen: true, shouldSelectWithTab: true});
  534. fireEvent.change(input, {target: {value: 'pine'}});
  535. fireEvent.keyDown(input, {key: 'Tab'});
  536. expect(mocks.onSelect).toHaveBeenCalledWith(
  537. items[1],
  538. expect.objectContaining({inputValue: 'pine', highlightedIndex: 0}),
  539. expect.anything()
  540. );
  541. expect(mocks.onClose).toHaveBeenCalledTimes(1);
  542. expect(input).toHaveValue('Pineapple');
  543. });
  544. it('does not reset highlight state if `closeOnSelect` is false and we select a new item', function () {
  545. createWrapper({closeOnSelect: false});
  546. jest.useFakeTimers();
  547. fireEvent.focus(input);
  548. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  549. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  550. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  551. // Select item
  552. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  553. // Should still remain open with same highlightedIndex
  554. expect(screen.getByText('Pineapple')).toHaveAttribute('aria-selected', 'true');
  555. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  556. fireEvent.keyDown(input, {key: 'ArrowDown', charCode: 40});
  557. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  558. // Select item
  559. fireEvent.keyDown(input, {key: 'Enter', charCode: 13});
  560. // Should still remain open with same highlightedIndex
  561. expect(screen.getByText('Orange')).toHaveAttribute('aria-selected', 'true');
  562. expect(screen.getByTestId('test-autocomplete')).toBeInTheDocument();
  563. });
  564. });