dropdownLink.spec.tsx 8.6 KB

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