dropdownLink.spec.tsx 8.0 KB

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