index.spec.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. import {Fragment} from 'react';
  2. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import {CompactSelect} from 'sentry/components/compactSelect';
  4. describe('CompactSelect', function () {
  5. it('renders', function () {
  6. const {container} = render(
  7. <CompactSelect
  8. options={[
  9. {value: 'opt_one', label: 'Option One'},
  10. {value: 'opt_two', label: 'Option Two'},
  11. ]}
  12. />
  13. );
  14. expect(container).toSnapshot();
  15. });
  16. it('renders disabled', function () {
  17. render(
  18. <CompactSelect
  19. disabled
  20. options={[
  21. {value: 'opt_one', label: 'Option One'},
  22. {value: 'opt_two', label: 'Option Two'},
  23. ]}
  24. />
  25. );
  26. expect(screen.getByRole('button')).toBeDisabled();
  27. });
  28. it('renders with menu title', async function () {
  29. render(
  30. <CompactSelect
  31. menuTitle="Menu title"
  32. options={[
  33. {value: 'opt_one', label: 'Option One'},
  34. {value: 'opt_two', label: 'Option Two'},
  35. ]}
  36. />
  37. );
  38. // click on the trigger button
  39. await userEvent.click(screen.getByRole('button'));
  40. expect(screen.getByText('Menu title')).toBeInTheDocument();
  41. });
  42. it('can be dismissed', async function () {
  43. render(
  44. <Fragment>
  45. <CompactSelect
  46. value="opt_one"
  47. menuTitle="Menu A"
  48. options={[
  49. {value: 'opt_one', label: 'Option One'},
  50. {value: 'opt_two', label: 'Option Two'},
  51. ]}
  52. />
  53. <CompactSelect
  54. value="opt_three"
  55. menuTitle="Menu B"
  56. options={[
  57. {value: 'opt_three', label: 'Option Three'},
  58. {value: 'opt_four', label: 'Option Four'},
  59. ]}
  60. />
  61. </Fragment>
  62. );
  63. // Can be dismissed by clicking outside
  64. await userEvent.click(screen.getByRole('button', {name: 'Option One'}));
  65. expect(screen.getByRole('option', {name: 'Option One'})).toBeInTheDocument();
  66. await userEvent.click(document.body);
  67. expect(screen.queryByRole('option', {name: 'Option One'})).not.toBeInTheDocument();
  68. // Can be dismissed by pressing Escape
  69. await userEvent.click(screen.getByRole('button', {name: 'Option One'}));
  70. expect(screen.getByRole('option', {name: 'Option One'})).toBeInTheDocument();
  71. await userEvent.keyboard('{Escape}');
  72. expect(screen.queryByRole('option', {name: 'Option One'})).not.toBeInTheDocument();
  73. // When menu A is open, clicking once on menu B's trigger button closes menu A and
  74. // then opens menu B
  75. await userEvent.click(screen.getByRole('button', {name: 'Option One'}));
  76. expect(screen.getByRole('option', {name: 'Option One'})).toBeInTheDocument();
  77. await userEvent.click(screen.getByRole('button', {name: 'Option Three'}));
  78. expect(screen.queryByRole('option', {name: 'Option One'})).not.toBeInTheDocument();
  79. expect(screen.getByRole('option', {name: 'Option Three'})).toBeInTheDocument();
  80. });
  81. describe('ListBox', function () {
  82. it('updates trigger label on selection', async function () {
  83. const mock = jest.fn();
  84. render(
  85. <CompactSelect
  86. options={[
  87. {value: 'opt_one', label: 'Option One'},
  88. {value: 'opt_two', label: 'Option Two'},
  89. ]}
  90. onChange={mock}
  91. />
  92. );
  93. // click on the trigger button
  94. await userEvent.click(screen.getByRole('button'));
  95. // select Option One
  96. await userEvent.click(screen.getByRole('option', {name: 'Option One'}));
  97. expect(mock).toHaveBeenCalledWith({value: 'opt_one', label: 'Option One'});
  98. expect(screen.getByRole('button', {name: 'Option One'})).toBeInTheDocument();
  99. });
  100. it('can select multiple options', async function () {
  101. const mock = jest.fn();
  102. render(
  103. <CompactSelect
  104. multiple
  105. options={[
  106. {value: 'opt_one', label: 'Option One'},
  107. {value: 'opt_two', label: 'Option Two'},
  108. ]}
  109. onChange={mock}
  110. />
  111. );
  112. // click on the trigger button
  113. await userEvent.click(screen.getByRole('button'));
  114. // select Option One & Option Two
  115. await userEvent.click(screen.getByRole('option', {name: 'Option One'}));
  116. await userEvent.click(screen.getByRole('option', {name: 'Option Two'}));
  117. expect(mock).toHaveBeenCalledWith([
  118. {value: 'opt_one', label: 'Option One'},
  119. {value: 'opt_two', label: 'Option Two'},
  120. ]);
  121. expect(screen.getByRole('button', {name: 'Option One +1'})).toBeInTheDocument();
  122. });
  123. it('can select options with values containing quotes', async function () {
  124. const mock = jest.fn();
  125. render(
  126. <CompactSelect
  127. multiple
  128. options={[
  129. {value: '"opt_one"', label: 'Option One'},
  130. {value: '"opt_two"', label: 'Option Two'},
  131. ]}
  132. onChange={mock}
  133. />
  134. );
  135. // click on the trigger button
  136. await userEvent.click(screen.getByRole('button'));
  137. // select Option One & Option Two
  138. await userEvent.click(screen.getByRole('option', {name: 'Option One'}));
  139. await userEvent.click(screen.getByRole('option', {name: 'Option Two'}));
  140. expect(mock).toHaveBeenCalledWith([
  141. {value: '"opt_one"', label: 'Option One'},
  142. {value: '"opt_two"', label: 'Option Two'},
  143. ]);
  144. });
  145. it('displays trigger button with prefix', function () {
  146. render(
  147. <CompactSelect
  148. triggerProps={{prefix: 'Prefix'}}
  149. value="opt_one"
  150. options={[
  151. {value: 'opt_one', label: 'Option One'},
  152. {value: 'opt_two', label: 'Option Two'},
  153. ]}
  154. />
  155. );
  156. expect(screen.getByRole('button', {name: 'Prefix Option One'})).toBeInTheDocument();
  157. });
  158. it('can search', async function () {
  159. render(
  160. <CompactSelect
  161. searchable
  162. searchPlaceholder="Search here…"
  163. options={[
  164. {value: 'opt_one', label: 'Option One'},
  165. {value: 'opt_two', label: 'Option Two'},
  166. ]}
  167. />
  168. );
  169. // click on the trigger button
  170. await userEvent.click(screen.getByRole('button'));
  171. // type 'Two' into the search box
  172. await userEvent.click(screen.getByPlaceholderText('Search here…'));
  173. await userEvent.keyboard('Two');
  174. // only Option Two should be available, Option One should be filtered out
  175. expect(screen.getByRole('option', {name: 'Option Two'})).toBeInTheDocument();
  176. expect(screen.queryByRole('option', {name: 'Option One'})).not.toBeInTheDocument();
  177. });
  178. it('can limit the number of options', async function () {
  179. render(
  180. <CompactSelect
  181. sizeLimit={2}
  182. sizeLimitMessage="Use search for more options…"
  183. searchable
  184. options={[
  185. {value: 'opt_one', label: 'Option One'},
  186. {value: 'opt_two', label: 'Option Two'},
  187. {value: 'opt_three', label: 'Option Three'},
  188. ]}
  189. />
  190. );
  191. // click on the trigger button
  192. await userEvent.click(screen.getByRole('button'));
  193. // only the first two options should be visible due to `sizeLimit`
  194. expect(screen.getByRole('option', {name: 'Option One'})).toBeInTheDocument();
  195. expect(screen.getByRole('option', {name: 'Option Two'})).toBeInTheDocument();
  196. expect(
  197. screen.queryByRole('option', {name: 'Option Three'})
  198. ).not.toBeInTheDocument();
  199. // there's a message prompting the user to use search to find more options
  200. expect(screen.getByText('Use search for more options…')).toBeInTheDocument();
  201. // Option Three is not reachable via keyboard, focus wraps back to Option One
  202. await userEvent.keyboard(`{ArrowDown}`);
  203. expect(screen.getByRole('option', {name: 'Option One'})).toHaveFocus();
  204. await userEvent.keyboard(`{ArrowDown>2}`);
  205. expect(screen.getByRole('option', {name: 'Option One'})).toHaveFocus();
  206. // Option Three is still available via search
  207. await userEvent.type(screen.getByPlaceholderText('Search…'), 'three');
  208. expect(screen.getByRole('option', {name: 'Option Three'})).toBeInTheDocument();
  209. // the size limit message is gone during search
  210. expect(screen.queryByText('Use search for more options…')).not.toBeInTheDocument();
  211. });
  212. it('can toggle sections', async function () {
  213. const mock = jest.fn();
  214. render(
  215. <CompactSelect
  216. multiple
  217. onSectionToggle={mock}
  218. options={[
  219. {
  220. key: 'section-1',
  221. label: 'Section 1',
  222. showToggleAllButton: true,
  223. options: [
  224. {value: 'opt_one', label: 'Option One'},
  225. {value: 'opt_two', label: 'Option Two'},
  226. ],
  227. },
  228. {
  229. key: 'section-2',
  230. label: 'Section 2',
  231. showToggleAllButton: true,
  232. options: [
  233. {value: 'opt_three', label: 'Option Three'},
  234. {value: 'opt_four', label: 'Option Four'},
  235. ],
  236. },
  237. ]}
  238. />
  239. );
  240. // click on the trigger button
  241. await userEvent.click(screen.getByRole('button', {expanded: false}));
  242. await waitFor(() =>
  243. expect(screen.getByRole('option', {name: 'Option One'})).toHaveFocus()
  244. );
  245. // move focus to Section 1's toggle button and press it to select all
  246. await userEvent.keyboard('{Tab}');
  247. expect(screen.getByRole('button', {name: 'Select All in Section 1'})).toHaveFocus();
  248. await userEvent.keyboard('{Enter}');
  249. expect(screen.getByRole('option', {name: 'Option One'})).toHaveAttribute(
  250. 'aria-selected',
  251. 'true'
  252. );
  253. expect(screen.getByRole('option', {name: 'Option Two'})).toHaveAttribute(
  254. 'aria-selected',
  255. 'true'
  256. );
  257. expect(mock).toHaveBeenCalledWith(
  258. {
  259. key: 'section-1',
  260. label: 'Section 1',
  261. showToggleAllButton: true,
  262. options: [
  263. {key: 'opt_one', value: 'opt_one', label: 'Option One'},
  264. {key: 'opt_two', value: 'opt_two', label: 'Option Two'},
  265. ],
  266. },
  267. 'select'
  268. );
  269. // press Section 1's toggle button again to unselect all
  270. await userEvent.keyboard('{Enter}');
  271. expect(screen.getByRole('option', {name: 'Option One'})).toHaveAttribute(
  272. 'aria-selected',
  273. 'false'
  274. );
  275. expect(screen.getByRole('option', {name: 'Option Two'})).toHaveAttribute(
  276. 'aria-selected',
  277. 'false'
  278. );
  279. expect(mock).toHaveBeenCalledWith(
  280. {
  281. key: 'section-1',
  282. label: 'Section 1',
  283. showToggleAllButton: true,
  284. options: [
  285. {key: 'opt_one', value: 'opt_one', label: 'Option One'},
  286. {key: 'opt_two', value: 'opt_two', label: 'Option Two'},
  287. ],
  288. },
  289. 'unselect'
  290. );
  291. // move to Section 2's toggle button and select all
  292. await userEvent.keyboard('{Tab}');
  293. expect(screen.getByRole('button', {name: 'Select All in Section 2'})).toHaveFocus();
  294. await userEvent.keyboard('{Enter}');
  295. expect(screen.getByRole('option', {name: 'Option Three'})).toHaveAttribute(
  296. 'aria-selected',
  297. 'true'
  298. );
  299. expect(screen.getByRole('option', {name: 'Option Four'})).toHaveAttribute(
  300. 'aria-selected',
  301. 'true'
  302. );
  303. expect(mock).toHaveBeenCalledWith(
  304. {
  305. key: 'section-2',
  306. label: 'Section 2',
  307. showToggleAllButton: true,
  308. options: [
  309. {key: 'opt_three', value: 'opt_three', label: 'Option Three'},
  310. {key: 'opt_four', value: 'opt_four', label: 'Option Four'},
  311. ],
  312. },
  313. 'select'
  314. );
  315. });
  316. it('triggers onClose when the menu is closed if provided', async function () {
  317. const onCloseMock = jest.fn();
  318. render(
  319. <CompactSelect
  320. onClose={onCloseMock}
  321. options={[
  322. {value: 'opt_one', label: 'Option One'},
  323. {value: 'opt_two', label: 'Option Two'},
  324. ]}
  325. />
  326. );
  327. // click on the trigger button
  328. await userEvent.click(screen.getByRole('button'));
  329. expect(onCloseMock).not.toHaveBeenCalled();
  330. // close the menu
  331. await userEvent.click(document.body);
  332. expect(onCloseMock).toHaveBeenCalled();
  333. });
  334. });
  335. describe('GridList', function () {
  336. it('updates trigger label on selection', async function () {
  337. const mock = jest.fn();
  338. render(
  339. <CompactSelect
  340. grid
  341. options={[
  342. {value: 'opt_one', label: 'Option One'},
  343. {value: 'opt_two', label: 'Option Two'},
  344. ]}
  345. onChange={mock}
  346. />
  347. );
  348. // click on the trigger button
  349. await userEvent.click(screen.getByRole('button'));
  350. // select Option One
  351. await userEvent.click(screen.getByRole('row', {name: 'Option One'}));
  352. expect(mock).toHaveBeenCalledWith({value: 'opt_one', label: 'Option One'});
  353. expect(screen.getByRole('button', {name: 'Option One'})).toBeInTheDocument();
  354. });
  355. it('can select multiple options', async function () {
  356. const mock = jest.fn();
  357. render(
  358. <CompactSelect
  359. grid
  360. multiple
  361. options={[
  362. {value: 'opt_one', label: 'Option One'},
  363. {value: 'opt_two', label: 'Option Two'},
  364. ]}
  365. onChange={mock}
  366. />
  367. );
  368. // click on the trigger button
  369. await userEvent.click(screen.getByRole('button'));
  370. // select Option One & Option Two
  371. await userEvent.click(screen.getByRole('row', {name: 'Option One'}));
  372. await userEvent.click(screen.getByRole('row', {name: 'Option Two'}));
  373. expect(mock).toHaveBeenCalledWith([
  374. {value: 'opt_one', label: 'Option One'},
  375. {value: 'opt_two', label: 'Option Two'},
  376. ]);
  377. expect(screen.getByRole('button', {name: 'Option One +1'})).toBeInTheDocument();
  378. });
  379. it('can select options with values containing quotes', async function () {
  380. const mock = jest.fn();
  381. render(
  382. <CompactSelect
  383. grid
  384. multiple
  385. options={[
  386. {value: '"opt_one"', label: 'Option One'},
  387. {value: '"opt_two"', label: 'Option Two'},
  388. ]}
  389. onChange={mock}
  390. />
  391. );
  392. // click on the trigger button
  393. await userEvent.click(screen.getByRole('button'));
  394. // select Option One & Option Two
  395. await userEvent.click(screen.getByRole('row', {name: 'Option One'}));
  396. await userEvent.click(screen.getByRole('row', {name: 'Option Two'}));
  397. expect(mock).toHaveBeenCalledWith([
  398. {value: '"opt_one"', label: 'Option One'},
  399. {value: '"opt_two"', label: 'Option Two'},
  400. ]);
  401. });
  402. it('displays trigger button with prefix', function () {
  403. render(
  404. <CompactSelect
  405. grid
  406. triggerProps={{prefix: 'Prefix'}}
  407. value="opt_one"
  408. options={[
  409. {value: 'opt_one', label: 'Option One'},
  410. {value: 'opt_two', label: 'Option Two'},
  411. ]}
  412. />
  413. );
  414. expect(screen.getByRole('button', {name: 'Prefix Option One'})).toBeInTheDocument();
  415. });
  416. it('can search', async function () {
  417. render(
  418. <CompactSelect
  419. grid
  420. searchable
  421. searchPlaceholder="Search here…"
  422. options={[
  423. {value: 'opt_one', label: 'Option One'},
  424. {value: 'opt_two', label: 'Option Two'},
  425. ]}
  426. />
  427. );
  428. // click on the trigger button
  429. await userEvent.click(screen.getByRole('button'));
  430. // type 'Two' into the search box
  431. await userEvent.click(screen.getByPlaceholderText('Search here…'));
  432. await userEvent.keyboard('Two');
  433. // only Option Two should be available, Option One should be filtered out
  434. expect(screen.getByRole('row', {name: 'Option Two'})).toBeInTheDocument();
  435. expect(screen.queryByRole('row', {name: 'Option One'})).not.toBeInTheDocument();
  436. });
  437. it('can limit the number of options', async function () {
  438. render(
  439. <CompactSelect
  440. grid
  441. sizeLimit={2}
  442. sizeLimitMessage="Use search for more options…"
  443. searchable
  444. options={[
  445. {value: 'opt_one', label: 'Option One'},
  446. {value: 'opt_two', label: 'Option Two'},
  447. {value: 'opt_three', label: 'Option Three'},
  448. ]}
  449. />
  450. );
  451. // click on the trigger button
  452. await userEvent.click(screen.getByRole('button'));
  453. // only the first two options should be visible due to `sizeLimit`
  454. expect(screen.getByRole('row', {name: 'Option One'})).toBeInTheDocument();
  455. expect(screen.getByRole('row', {name: 'Option Two'})).toBeInTheDocument();
  456. expect(screen.queryByRole('row', {name: 'Option Three'})).not.toBeInTheDocument();
  457. // there's a message prompting the user to use search to find more options
  458. expect(screen.getByText('Use search for more options…')).toBeInTheDocument();
  459. // Option Three is not reachable via keyboard, focus wraps back to Option One
  460. await userEvent.keyboard(`{ArrowDown}`);
  461. expect(screen.getByRole('row', {name: 'Option One'})).toHaveFocus();
  462. await userEvent.keyboard(`{ArrowDown>2}`);
  463. expect(screen.getByRole('row', {name: 'Option One'})).toHaveFocus();
  464. // Option Three is still available via search
  465. await userEvent.type(screen.getByPlaceholderText('Search…'), 'three');
  466. expect(screen.getByRole('row', {name: 'Option Three'})).toBeInTheDocument();
  467. // the size limit message is gone during search
  468. expect(screen.queryByText('Use search for more options…')).not.toBeInTheDocument();
  469. });
  470. it('can toggle sections', async function () {
  471. const mock = jest.fn();
  472. render(
  473. <CompactSelect
  474. grid
  475. multiple
  476. onSectionToggle={mock}
  477. options={[
  478. {
  479. key: 'section-1',
  480. label: 'Section 1',
  481. showToggleAllButton: true,
  482. options: [
  483. {value: 'opt_one', label: 'Option One'},
  484. {value: 'opt_two', label: 'Option Two'},
  485. ],
  486. },
  487. {
  488. key: 'section-2',
  489. label: 'Section 2',
  490. showToggleAllButton: true,
  491. options: [
  492. {value: 'opt_three', label: 'Option Three'},
  493. {value: 'opt_four', label: 'Option Four'},
  494. ],
  495. },
  496. ]}
  497. />
  498. );
  499. // click on the trigger button
  500. await userEvent.click(screen.getByRole('button', {expanded: false}));
  501. await waitFor(() =>
  502. expect(screen.getByRole('row', {name: 'Option One'})).toHaveFocus()
  503. );
  504. // move focus to Section 1's toggle button and press it to select all
  505. await userEvent.keyboard('{Tab}');
  506. expect(screen.getByRole('button', {name: 'Select All in Section 1'})).toHaveFocus();
  507. await userEvent.keyboard('{Enter}');
  508. expect(screen.getByRole('row', {name: 'Option One'})).toHaveAttribute(
  509. 'aria-selected',
  510. 'true'
  511. );
  512. expect(screen.getByRole('row', {name: 'Option Two'})).toHaveAttribute(
  513. 'aria-selected',
  514. 'true'
  515. );
  516. expect(mock).toHaveBeenCalledWith(
  517. {
  518. key: 'section-1',
  519. label: 'Section 1',
  520. showToggleAllButton: true,
  521. options: [
  522. {key: 'opt_one', value: 'opt_one', label: 'Option One'},
  523. {key: 'opt_two', value: 'opt_two', label: 'Option Two'},
  524. ],
  525. },
  526. 'select'
  527. );
  528. // press Section 1's toggle button again to unselect all
  529. await userEvent.keyboard('{Enter}');
  530. expect(screen.getByRole('row', {name: 'Option One'})).toHaveAttribute(
  531. 'aria-selected',
  532. 'false'
  533. );
  534. expect(screen.getByRole('row', {name: 'Option Two'})).toHaveAttribute(
  535. 'aria-selected',
  536. 'false'
  537. );
  538. expect(mock).toHaveBeenCalledWith(
  539. {
  540. key: 'section-1',
  541. label: 'Section 1',
  542. showToggleAllButton: true,
  543. options: [
  544. {key: 'opt_one', value: 'opt_one', label: 'Option One'},
  545. {key: 'opt_two', value: 'opt_two', label: 'Option Two'},
  546. ],
  547. },
  548. 'unselect'
  549. );
  550. // move to Section 2's toggle button and select all
  551. await userEvent.keyboard('{Tab}');
  552. expect(screen.getByRole('button', {name: 'Select All in Section 2'})).toHaveFocus();
  553. await userEvent.keyboard('{Enter}');
  554. expect(screen.getByRole('row', {name: 'Option Three'})).toHaveAttribute(
  555. 'aria-selected',
  556. 'true'
  557. );
  558. expect(screen.getByRole('row', {name: 'Option Four'})).toHaveAttribute(
  559. 'aria-selected',
  560. 'true'
  561. );
  562. expect(mock).toHaveBeenCalledWith(
  563. {
  564. key: 'section-2',
  565. label: 'Section 2',
  566. showToggleAllButton: true,
  567. options: [
  568. {key: 'opt_three', value: 'opt_three', label: 'Option Three'},
  569. {key: 'opt_four', value: 'opt_four', label: 'Option Four'},
  570. ],
  571. },
  572. 'select'
  573. );
  574. });
  575. it('triggers onClose when the menu is closed if provided', async function () {
  576. const onCloseMock = jest.fn();
  577. render(
  578. <CompactSelect
  579. grid
  580. onClose={onCloseMock}
  581. options={[
  582. {value: 'opt_one', label: 'Option One'},
  583. {value: 'opt_two', label: 'Option Two'},
  584. ]}
  585. />
  586. );
  587. // click on the trigger button
  588. await userEvent.click(screen.getByRole('button'));
  589. expect(onCloseMock).not.toHaveBeenCalled();
  590. // close the menu
  591. await userEvent.click(document.body);
  592. expect(onCloseMock).toHaveBeenCalled();
  593. });
  594. it('allows keyboard navigation to nested buttons', async function () {
  595. const onPointerUpMock = jest.fn();
  596. const onKeyUpMock = jest.fn();
  597. render(
  598. <CompactSelect
  599. grid
  600. options={[
  601. {
  602. value: 'opt_one',
  603. label: 'Option One',
  604. trailingItems: (
  605. <button onPointerUp={onPointerUpMock} onKeyUp={onKeyUpMock}>
  606. Trailing Button One
  607. </button>
  608. ),
  609. },
  610. {
  611. value: 'opt_two',
  612. label: 'Option Two',
  613. trailingItems: (
  614. <button onPointerUp={onPointerUpMock} onKeyUp={onKeyUpMock}>
  615. Trailing Button Two
  616. </button>
  617. ),
  618. },
  619. ]}
  620. />
  621. );
  622. // click on the trigger button
  623. await userEvent.click(screen.getByRole('button'));
  624. await waitFor(() =>
  625. expect(screen.getByRole('row', {name: 'Option One'})).toHaveFocus()
  626. );
  627. // press Arrow Right, focus should be moved to the trailing button
  628. await userEvent.keyboard('{ArrowRight}');
  629. expect(screen.getByRole('button', {name: 'Trailing Button One'})).toHaveFocus();
  630. // press Enter, onKeyUpMock is called
  631. await userEvent.keyboard('{ArrowRight}');
  632. expect(onKeyUpMock).toHaveBeenCalled();
  633. // click on Trailing Button Two, onPointerUpMock is called
  634. await userEvent.click(screen.getByRole('button', {name: 'Trailing Button Two'}));
  635. expect(onPointerUpMock).toHaveBeenCalled();
  636. });
  637. });
  638. });