You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SubnavigationIssues-it.tsx 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { screen } from '@testing-library/react';
  21. import userEvent from '@testing-library/user-event';
  22. import * as React from 'react';
  23. import { mockFlowLocation, mockIssue, mockPaging } from '../../../../helpers/testMocks';
  24. import { renderComponent } from '../../../../helpers/testReactTestingUtils';
  25. import { byRole } from '../../../../helpers/testSelector';
  26. import { ComponentPropsType } from '../../../../helpers/testUtils';
  27. import { FlowType, Issue } from '../../../../types/types';
  28. import { VISIBLE_LOCATIONS_COLLAPSE } from '../IssueLocationsCrossFile';
  29. import SubnavigationIssuesList from '../SubnavigationIssuesList';
  30. const loc = mockFlowLocation();
  31. const issues = [
  32. mockIssue(false, {
  33. key: 'issue1',
  34. message: 'Issue 1',
  35. component: 'foo',
  36. componentLongName: 'Long Foo',
  37. }),
  38. mockIssue(false, {
  39. key: 'issue2',
  40. message: 'Issue 2',
  41. component: 'foo',
  42. componentLongName: 'Long Foo',
  43. }),
  44. mockIssue(false, {
  45. key: 'issue3',
  46. message: 'Issue 3',
  47. component: 'bar',
  48. componentLongName: 'Long Bar',
  49. }),
  50. mockIssue(false, {
  51. key: 'issue4',
  52. message: 'Issue 4',
  53. component: 'foo',
  54. componentLongName: 'Long Foo',
  55. flowsWithType: [
  56. {
  57. type: FlowType.DATA,
  58. description: 'Flow Foo',
  59. locations: [mockFlowLocation({ msg: 'loc 1' })],
  60. },
  61. ],
  62. }),
  63. ];
  64. describe('rendering', () => {
  65. it('should render concise issues without duplicating component', () => {
  66. renderConciseIssues(issues);
  67. expect(screen.getAllByTitle('Long Foo')).toHaveLength(2);
  68. expect(screen.getByTitle('Long Bar')).toBeInTheDocument();
  69. });
  70. it('should scroll issue into view when one of the issue is selected', () => {
  71. renderConciseIssues(issues, {
  72. selected: 'issue2',
  73. });
  74. });
  75. it('should show locations and flows when selected', () => {
  76. renderConciseIssues(issues, {
  77. selected: 'issue4',
  78. selectedFlowIndex: 0,
  79. });
  80. expect(screen.getByText('Flow Foo')).toBeInTheDocument();
  81. expect(screen.getByText('loc 1')).toBeInTheDocument();
  82. });
  83. it('should hide locations and flows when not selected', () => {
  84. renderConciseIssues(issues, {
  85. selected: 'issue2',
  86. });
  87. expect(screen.queryByText('Flow Foo')).not.toBeInTheDocument();
  88. expect(screen.queryByText('loc 1')).not.toBeInTheDocument();
  89. });
  90. it('should not render the expand button if below the collapse limit', () => {
  91. const { ui } = getPageObject();
  92. renderConciseIssues(
  93. [
  94. ...issues,
  95. mockIssue(false, {
  96. key: 'custom',
  97. message: 'Custom Issue',
  98. flows: Array.from({ length: VISIBLE_LOCATIONS_COLLAPSE }).map((i) => [
  99. mockFlowLocation({ component: `component-${i}` }),
  100. ]),
  101. }),
  102. ],
  103. {
  104. selected: 'custom',
  105. },
  106. );
  107. expect(ui.expandBadgesButton.query()).not.toBeInTheDocument();
  108. });
  109. });
  110. describe('interacting', () => {
  111. it('should scroll selected issue into view', () => {
  112. const scrollIntoView = jest.fn();
  113. const globalScrollView = window.HTMLElement.prototype.scrollIntoView;
  114. window.HTMLElement.prototype.scrollIntoView = scrollIntoView;
  115. const { override } = renderConciseIssues(issues, {
  116. selected: 'issue2',
  117. });
  118. expect(scrollIntoView).toHaveBeenCalledTimes(1);
  119. override(issues, {
  120. selected: 'issue4',
  121. });
  122. expect(scrollIntoView).toHaveBeenCalledTimes(2);
  123. window.HTMLElement.prototype.scrollIntoView = globalScrollView;
  124. });
  125. it('expand button should work correctly', async () => {
  126. const { ui } = getPageObject();
  127. const flow = Array.from({ length: VISIBLE_LOCATIONS_COLLAPSE + 1 }).map((_, i) =>
  128. mockFlowLocation({
  129. component: `component-${i}`,
  130. index: i,
  131. msg: `loc ${i}`,
  132. }),
  133. );
  134. renderConciseIssues(
  135. [
  136. mockIssue(false, {
  137. key: 'custom',
  138. component: 'issue-component',
  139. message: 'Custom Issue',
  140. flows: [flow],
  141. }),
  142. ],
  143. {
  144. selected: 'custom',
  145. selectedFlowIndex: 0,
  146. },
  147. );
  148. expect(ui.expandBadgesButton.get()).toBeInTheDocument();
  149. expect(screen.getAllByText(/loc \d/)).toHaveLength(VISIBLE_LOCATIONS_COLLAPSE);
  150. await ui.clickExpandBadgesButton();
  151. expect(ui.expandBadgesButton.query()).not.toBeInTheDocument();
  152. expect(screen.getAllByText(/loc \d/)).toHaveLength(VISIBLE_LOCATIONS_COLLAPSE + 1);
  153. });
  154. it('issue selection should correctly be handled', async () => {
  155. const { user } = getPageObject();
  156. const onIssueSelect = jest.fn();
  157. renderConciseIssues(issues, {
  158. onIssueSelect,
  159. selected: 'issue2',
  160. });
  161. expect(onIssueSelect).not.toHaveBeenCalled();
  162. await user.click(screen.getByRole('button', { name: 'Issue 4' }));
  163. expect(onIssueSelect).toHaveBeenCalledTimes(1);
  164. expect(onIssueSelect).toHaveBeenLastCalledWith('issue4');
  165. });
  166. it('flow selection should correctly be handled', async () => {
  167. const { user } = getPageObject();
  168. const onFlowSelect = jest.fn();
  169. renderConciseIssues(
  170. [
  171. ...issues,
  172. mockIssue(false, {
  173. key: 'custom',
  174. message: 'Custom Issue',
  175. secondaryLocations: [],
  176. flows: [[loc], [loc, loc, loc], [loc]],
  177. }),
  178. ],
  179. {
  180. onFlowSelect,
  181. selected: 'custom',
  182. },
  183. );
  184. expect(onFlowSelect).not.toHaveBeenCalled();
  185. await user.click(screen.getByText('issue.flow.x_steps.3'));
  186. expect(onFlowSelect).toHaveBeenCalledTimes(1);
  187. expect(onFlowSelect).toHaveBeenLastCalledWith(1);
  188. });
  189. });
  190. function getPageObject() {
  191. const selectors = {
  192. headerBackButton: byRole('link', { name: 'issues.return_to_list' }),
  193. expandBadgesButton: byRole('button', { name: /issues.show_x_more_locations.\d/ }),
  194. };
  195. const user = userEvent.setup();
  196. const ui = {
  197. ...selectors,
  198. async clickBackButton() {
  199. await user.click(ui.headerBackButton.get());
  200. },
  201. async clickExpandBadgesButton() {
  202. await user.click(ui.expandBadgesButton.get());
  203. },
  204. };
  205. return { ui, user };
  206. }
  207. function renderConciseIssues(
  208. issues: Issue[],
  209. listProps: Partial<ComponentPropsType<typeof SubnavigationIssuesList>> = {},
  210. ) {
  211. const wrapper = renderComponent(
  212. <SubnavigationIssuesList
  213. fetchMoreIssues={jest.fn()}
  214. loading={false}
  215. loadingMore={false}
  216. paging={mockPaging({ total: 10 })}
  217. issues={issues}
  218. onFlowSelect={jest.fn()}
  219. onIssueSelect={jest.fn()}
  220. onLocationSelect={jest.fn()}
  221. selected={undefined}
  222. selectedFlowIndex={undefined}
  223. selectedLocationIndex={undefined}
  224. {...listProps}
  225. />,
  226. );
  227. function override(
  228. issues: Issue[],
  229. listProps: Partial<ComponentPropsType<typeof SubnavigationIssuesList>> = {},
  230. ) {
  231. wrapper.rerender(
  232. <SubnavigationIssuesList
  233. fetchMoreIssues={jest.fn()}
  234. issues={issues}
  235. loading={false}
  236. loadingMore={false}
  237. paging={mockPaging({ total: 10 })}
  238. onFlowSelect={jest.fn()}
  239. onIssueSelect={jest.fn()}
  240. onLocationSelect={jest.fn()}
  241. selected={undefined}
  242. selectedFlowIndex={undefined}
  243. selectedLocationIndex={undefined}
  244. {...listProps}
  245. />,
  246. );
  247. }
  248. return { ...wrapper, override };
  249. }