dropdownLink.spec.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import {
  2. render,
  3. screen,
  4. userEvent,
  5. waitForElementToBeRemoved,
  6. } from 'sentry-test/reactTestingLibrary';
  7. import DropdownLink from 'sentry/components/dropdownLink';
  8. import {MENU_CLOSE_DELAY} from 'sentry/constants';
  9. describe('DropdownLink', function () {
  10. beforeEach(() => {
  11. jest.useFakeTimers();
  12. });
  13. afterEach(() => {
  14. jest.useRealTimers();
  15. });
  16. const INPUT_1 = {
  17. title: 'test',
  18. onOpen: () => {},
  19. onClose: () => {},
  20. topLevelClasses: 'top-level-class',
  21. alwaysRenderMenu: true,
  22. menuClasses: '',
  23. };
  24. describe('renders', function () {
  25. it('and anchors to left by default', function () {
  26. render(
  27. <DropdownLink {...INPUT_1}>
  28. <div>1</div>
  29. <div>2</div>
  30. </DropdownLink>
  31. );
  32. });
  33. it('and anchors to right', function () {
  34. render(
  35. <DropdownLink {...INPUT_1} anchorRight>
  36. <div>1</div>
  37. <div>2</div>
  38. </DropdownLink>
  39. );
  40. });
  41. });
  42. describe('Uncontrolled', function () {
  43. describe('While Closed', function () {
  44. it('displays dropdown menu when dropdown actor button clicked', async function () {
  45. render(
  46. <DropdownLink alwaysRenderMenu={false} title="test">
  47. <li>hi</li>
  48. </DropdownLink>
  49. );
  50. expect(screen.queryByText('hi')).not.toBeInTheDocument();
  51. // open
  52. await userEvent.click(screen.getByText('test'), {delay: null});
  53. expect(screen.getByText('hi')).toBeInTheDocument();
  54. });
  55. });
  56. describe('While Opened', function () {
  57. it('closes when clicked outside', async function () {
  58. render(
  59. <div data-test-id="outside-element">
  60. <DropdownLink title="test" alwaysRenderMenu={false}>
  61. <li>hi</li>
  62. </DropdownLink>
  63. </div>
  64. );
  65. // Open menu
  66. await userEvent.click(screen.getByText('test'), {delay: null});
  67. // Click outside
  68. await userEvent.click(screen.getByTestId('outside-element'), {delay: null});
  69. await waitForElementToBeRemoved(() => screen.queryByText('hi'));
  70. });
  71. it('closes when dropdown actor button is clicked', async function () {
  72. render(
  73. <DropdownLink title="test" alwaysRenderMenu={false}>
  74. <li>hi</li>
  75. </DropdownLink>
  76. );
  77. // Open menu
  78. await userEvent.click(screen.getByText('test'), {delay: null});
  79. // Click again
  80. await userEvent.click(screen.getByText('test'), {delay: null});
  81. expect(screen.queryByText('hi')).not.toBeInTheDocument();
  82. });
  83. it('closes when dropdown menu item is clicked', async function () {
  84. render(
  85. <DropdownLink title="test" alwaysRenderMenu={false}>
  86. <li>hi</li>
  87. </DropdownLink>
  88. );
  89. // Open menu
  90. await userEvent.click(screen.getByText('test'), {delay: null});
  91. await userEvent.click(screen.getByText('hi'), {delay: null});
  92. expect(screen.queryByText('hi')).not.toBeInTheDocument();
  93. });
  94. it('does not close when menu is clicked and `keepMenuOpen` is on', async function () {
  95. render(
  96. <DropdownLink title="test" alwaysRenderMenu={false} keepMenuOpen>
  97. <li>hi</li>
  98. </DropdownLink>
  99. );
  100. // Open menu
  101. await userEvent.click(screen.getByText('test'), {delay: null});
  102. // Click again
  103. await userEvent.click(screen.getByText('test'), {delay: null});
  104. expect(screen.getByText('test')).toBeInTheDocument();
  105. });
  106. });
  107. });
  108. describe('Controlled', function () {
  109. describe('Opened', function () {
  110. it('does not close when menu is clicked', async function () {
  111. render(
  112. <DropdownLink title="test" alwaysRenderMenu={false} isOpen>
  113. <li>hi</li>
  114. </DropdownLink>
  115. );
  116. // Click option
  117. await userEvent.click(screen.getByText('hi'), {delay: null});
  118. // Should still be open
  119. expect(screen.getByText('hi')).toBeInTheDocument();
  120. });
  121. it('does not close when document is clicked', async function () {
  122. render(
  123. <div data-test-id="outside-element">
  124. <DropdownLink title="test" alwaysRenderMenu={false} isOpen>
  125. <li>hi</li>
  126. </DropdownLink>
  127. </div>
  128. );
  129. // Click outside
  130. await userEvent.click(screen.getByTestId('outside-element'), {delay: null});
  131. // Should still be open
  132. expect(screen.getByText('hi')).toBeInTheDocument();
  133. });
  134. it('does not close when dropdown actor is clicked', async function () {
  135. render(
  136. <DropdownLink title="test" alwaysRenderMenu={false} isOpen>
  137. <li>hi</li>
  138. </DropdownLink>
  139. );
  140. // Click menu
  141. await userEvent.click(screen.getByText('test'), {delay: null});
  142. // Should still be open
  143. expect(screen.getByText('hi')).toBeInTheDocument();
  144. });
  145. });
  146. describe('Closed', function () {
  147. it('does not open when dropdown actor is clicked', async function () {
  148. render(
  149. <DropdownLink title="test" alwaysRenderMenu={false} isOpen={false}>
  150. <li>hi</li>
  151. </DropdownLink>
  152. );
  153. // Click menu
  154. await userEvent.click(screen.getByText('test'), {delay: null});
  155. expect(screen.queryByText('hi')).not.toBeInTheDocument();
  156. });
  157. });
  158. });
  159. describe('Nested Dropdown', function () {
  160. function NestedDropdown() {
  161. return (
  162. <DropdownLink title="parent" alwaysRenderMenu={false}>
  163. <li>
  164. <DropdownLink alwaysRenderMenu={false} title="nested" isNestedDropdown>
  165. <li>
  166. <DropdownLink alwaysRenderMenu={false} title="nested #2" isNestedDropdown>
  167. <li>Hello</li>
  168. </DropdownLink>
  169. </li>
  170. </DropdownLink>
  171. </li>
  172. <li id="no-nest">Item 2</li>
  173. </DropdownLink>
  174. );
  175. }
  176. it('closes when top-level actor is clicked', async function () {
  177. render(<NestedDropdown />);
  178. // Open menu
  179. await userEvent.click(screen.getByText('parent'), {delay: null});
  180. // Close menu
  181. await userEvent.click(screen.getByText('parent'), {delay: null});
  182. expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
  183. });
  184. it('Opens / closes on mouse enter and leave', async function () {
  185. render(<NestedDropdown />);
  186. // Open menu
  187. await userEvent.click(screen.getByText('parent'), {delay: null});
  188. await userEvent.hover(screen.getByText('nested'), {delay: null});
  189. await screen.findByText('nested #2');
  190. // Leaving Nested Menu
  191. await userEvent.unhover(screen.getByText('nested'), {delay: null});
  192. // Nested menus have close delay
  193. jest.advanceTimersByTime(MENU_CLOSE_DELAY - 1);
  194. // Re-entering nested menu will cancel close
  195. await userEvent.hover(screen.getByText('nested'), {delay: null});
  196. jest.advanceTimersByTime(2);
  197. expect(screen.getByText('nested #2')).toBeInTheDocument();
  198. // Re-entering an actor will also cancel close
  199. jest.advanceTimersByTime(MENU_CLOSE_DELAY - 1);
  200. jest.advanceTimersByTime(2);
  201. await userEvent.hover(screen.getByText('parent'), {delay: null});
  202. expect(screen.getByText('nested #2')).toBeInTheDocument();
  203. // Leave menu
  204. await userEvent.unhover(screen.getByText('nested'), {delay: null});
  205. jest.runAllTimers();
  206. expect(screen.queryByText('nested #2')).not.toBeInTheDocument();
  207. });
  208. it('does not close when nested actors are clicked', async function () {
  209. render(<NestedDropdown />);
  210. // Open menu
  211. await userEvent.click(screen.getByText('parent'), {delay: null});
  212. await userEvent.click(screen.getByText('nested'), {delay: null});
  213. expect(screen.getByRole('listbox')).toBeInTheDocument();
  214. await userEvent.hover(screen.getByText('nested'), {delay: null});
  215. await userEvent.click(await screen.findByText('nested #2'), {delay: null});
  216. expect(screen.getAllByRole('listbox')).toHaveLength(2);
  217. });
  218. it('closes when terminal nested actor is clicked', async function () {
  219. render(<NestedDropdown />);
  220. // Open menu
  221. await userEvent.click(screen.getByText('parent'), {delay: null});
  222. await userEvent.hover(screen.getByText('nested'), {delay: null});
  223. await userEvent.hover(await screen.findByText('nested #2'), {delay: null});
  224. await userEvent.click(await screen.findByText('Hello'), {delay: null});
  225. expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
  226. });
  227. });
  228. });