index.spec.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {Fragment} from 'react';
  2. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  3. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  4. describe('DropdownMenu', function () {
  5. it('renders a basic menu', async function () {
  6. const onAction = jest.fn();
  7. render(
  8. <DropdownMenu
  9. items={[
  10. {
  11. key: 'item1',
  12. label: 'Item One',
  13. details: 'This is the first item',
  14. onAction,
  15. },
  16. {
  17. key: 'item2',
  18. label: 'Item Two',
  19. details: 'Another description here',
  20. },
  21. ]}
  22. triggerLabel="This is a Menu"
  23. />
  24. );
  25. // Open the mneu
  26. await userEvent.click(screen.getByRole('button', {name: 'This is a Menu'}));
  27. // The menu is open
  28. expect(screen.getByRole('menu')).toBeInTheDocument();
  29. // There are two menu items
  30. //
  31. // TODO(epurkhiser): These should really be menuitem roles NOT
  32. // menuitemradio's. But react-aria is setting this for us (probably because
  33. // the menu has submenus, so we need to be able to "select" them). We
  34. // should figure out how to tell it that this menu does not allow
  35. expect(screen.getAllByRole('menuitemradio')).toHaveLength(2);
  36. expect(
  37. screen.getByRole('menuitemradio', {name: 'Item One'})
  38. ).toHaveAccessibleDescription('This is the first item');
  39. expect(
  40. screen.getByRole('menuitemradio', {name: 'Item Two'})
  41. ).toHaveAccessibleDescription('Another description here');
  42. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Item One'}));
  43. expect(onAction).toHaveBeenCalled();
  44. });
  45. it('renders disabled items', async function () {
  46. const onAction = jest.fn();
  47. render(
  48. <DropdownMenu
  49. items={[
  50. {
  51. key: 'item1',
  52. label: 'Item One',
  53. disabled: true,
  54. onAction,
  55. },
  56. ]}
  57. triggerLabel="Menu"
  58. />
  59. );
  60. await userEvent.click(screen.getByRole('button', {name: 'Menu'}));
  61. const menuItem = screen.getByRole('menuitemradio', {name: 'Item One'});
  62. // RTL doesn't support toBeDisabled for aria-disabled
  63. //
  64. // See: https://github.com/testing-library/jest-dom/issues/144#issuecomment-577235097
  65. expect(menuItem).toHaveAttribute('aria-disabled', 'true');
  66. await userEvent.click(menuItem);
  67. expect(onAction).not.toHaveBeenCalled();
  68. });
  69. it('can be dismissed', async function () {
  70. render(
  71. <Fragment>
  72. <DropdownMenu items={[{key: 'item1', label: 'Item One'}]} triggerLabel="Menu A" />
  73. <DropdownMenu items={[{key: 'item2', label: 'Item Two'}]} triggerLabel="Menu B" />
  74. </Fragment>
  75. );
  76. // Can be dismissed by clicking outside
  77. await userEvent.click(screen.getByRole('button', {name: 'Menu A'}));
  78. expect(screen.getByRole('menuitemradio', {name: 'Item One'})).toBeInTheDocument();
  79. await userEvent.click(document.body);
  80. expect(
  81. screen.queryByRole('menuitemradio', {name: 'Item One'})
  82. ).not.toBeInTheDocument();
  83. // Can be dismissed by pressing Escape
  84. await userEvent.click(screen.getByRole('button', {name: 'Menu A'}));
  85. expect(screen.getByRole('menuitemradio', {name: 'Item One'})).toBeInTheDocument();
  86. await userEvent.keyboard('{Escape}');
  87. expect(
  88. screen.queryByRole('menuitemradio', {name: 'Item One'})
  89. ).not.toBeInTheDocument();
  90. // When menu A is open, clicking once on menu B's trigger button closes menu A and
  91. // then opens menu B
  92. await userEvent.click(screen.getByRole('button', {name: 'Menu A'}));
  93. expect(screen.getByRole('menuitemradio', {name: 'Item One'})).toBeInTheDocument();
  94. await userEvent.click(screen.getByRole('button', {name: 'Menu B'}));
  95. expect(
  96. screen.queryByRole('menuitemradio', {name: 'Item One'})
  97. ).not.toBeInTheDocument();
  98. expect(screen.getByRole('menuitemradio', {name: 'Item Two'})).toBeInTheDocument();
  99. });
  100. it('renders submenus', async function () {
  101. const onAction = jest.fn();
  102. render(
  103. <DropdownMenu
  104. items={[
  105. {
  106. key: 'item1',
  107. label: 'Item',
  108. isSubmenu: true,
  109. children: [
  110. {
  111. key: 'subitem',
  112. label: 'Sub Item',
  113. onAction,
  114. },
  115. ],
  116. },
  117. {
  118. key: 'item2',
  119. label: 'Item Two',
  120. },
  121. ]}
  122. triggerLabel="Menu"
  123. />
  124. );
  125. await userEvent.click(screen.getByRole('button', {name: 'Menu'}));
  126. // Sub item won't be visible until we hover over its parent
  127. expect(
  128. screen.queryByRole('menuitemradio', {name: 'Sub Item'})
  129. ).not.toBeInTheDocument();
  130. const parentItem = screen.getByRole('menuitemradio', {name: 'Item'});
  131. expect(parentItem).toHaveAttribute('aria-haspopup', 'true');
  132. expect(parentItem).toHaveAttribute('aria-expanded', 'false');
  133. await userEvent.hover(parentItem);
  134. // The sub item is now visibile
  135. const subItem = screen.getByRole('menuitemradio', {name: 'Sub Item'});
  136. expect(subItem).toBeInTheDocument();
  137. // Menu does not close when hovering over it
  138. await userEvent.unhover(parentItem);
  139. await userEvent.hover(subItem);
  140. expect(subItem).toBeInTheDocument();
  141. // Menu is closed when hovering the other menu item
  142. await userEvent.unhover(subItem);
  143. await userEvent.hover(screen.getByRole('menuitemradio', {name: 'Item Two'}));
  144. expect(subItem).not.toBeInTheDocument();
  145. // Click the menu item
  146. await userEvent.hover(parentItem);
  147. await userEvent.click(screen.getByRole('menuitemradio', {name: 'Sub Item'}));
  148. expect(onAction).toHaveBeenCalled();
  149. // Entire menu system is closed
  150. expect(screen.getByRole('button', {name: 'Menu'})).toHaveAttribute(
  151. 'aria-expanded',
  152. 'false'
  153. );
  154. // Pressing Esc closes the entire menu system
  155. await userEvent.click(screen.getByRole('button', {name: 'Menu'}));
  156. await userEvent.hover(screen.getByRole('menuitemradio', {name: 'Item'}));
  157. await userEvent.hover(screen.getByRole('menuitemradio', {name: 'Sub Item'}));
  158. await userEvent.keyboard('{Escape}');
  159. expect(screen.getByRole('button', {name: 'Menu'})).toHaveAttribute(
  160. 'aria-expanded',
  161. 'false'
  162. );
  163. // Clicking outside closes the entire menu system
  164. await userEvent.click(screen.getByRole('button', {name: 'Menu'}));
  165. await userEvent.hover(screen.getByRole('menuitemradio', {name: 'Item'}));
  166. await userEvent.hover(screen.getByRole('menuitemradio', {name: 'Sub Item'}));
  167. await userEvent.click(document.body);
  168. expect(screen.getByRole('button', {name: 'Menu'})).toHaveAttribute(
  169. 'aria-expanded',
  170. 'false'
  171. );
  172. });
  173. });