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.

IssueApp-it.tsx 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 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 { act, screen, within } from '@testing-library/react';
  21. import userEvent from '@testing-library/user-event';
  22. import React from 'react';
  23. import { TabKeys } from '../../../components/rules/RuleTabViewer';
  24. import { byRole } from '../../../helpers/testSelector';
  25. import {
  26. branchHandler,
  27. componentsHandler,
  28. issuesHandler,
  29. renderIssueApp,
  30. renderProjectIssuesApp,
  31. ui,
  32. } from '../test-utils';
  33. jest.mock('../sidebar/Sidebar', () => {
  34. const fakeSidebar = () => {
  35. return <div data-guiding-id="issue-5" />;
  36. };
  37. return {
  38. __esModule: true,
  39. default: fakeSidebar,
  40. Sidebar: fakeSidebar,
  41. };
  42. });
  43. jest.mock('../../../components/common/ScreenPositionHelper', () => ({
  44. __esModule: true,
  45. default: class ScreenPositionHelper extends React.Component<{
  46. children: (args: { top: number }) => React.ReactNode;
  47. }> {
  48. render() {
  49. // eslint-disable-next-line testing-library/no-node-access
  50. return this.props.children({ top: 10 });
  51. }
  52. },
  53. }));
  54. beforeEach(() => {
  55. issuesHandler.reset();
  56. componentsHandler.reset();
  57. branchHandler.reset();
  58. window.scrollTo = jest.fn();
  59. window.HTMLElement.prototype.scrollTo = jest.fn();
  60. });
  61. describe('issue app', () => {
  62. it('should navigate to Why is this an issue tab', async () => {
  63. renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject&why=1');
  64. expect(
  65. await screen.findByRole('tab', {
  66. name: `coding_rules.description_section.title.root_cause`,
  67. }),
  68. ).toHaveAttribute('aria-current', 'true');
  69. });
  70. it('should interact with flows and locations', async () => {
  71. const user = userEvent.setup();
  72. renderProjectIssuesApp('project/issues?issues=issue11&open=issue11&id=myproject');
  73. expect(await screen.findByLabelText('list_of_issues')).toBeInTheDocument();
  74. const dataFlowButton = await screen.findByRole('button', {
  75. name: 'issue.flow.x_steps.2 Backtracking 1',
  76. });
  77. const exectionFlowButton = screen.getByRole('button', {
  78. name: 'issue.flow.x_steps.3 issue.full_execution_flow',
  79. });
  80. let dataLocation1Button = screen.getByRole('link', { name: '1 Data location 1' });
  81. let dataLocation2Button = screen.getByRole('link', { name: '2 Data location 2' });
  82. expect(dataFlowButton).toBeInTheDocument();
  83. expect(dataLocation1Button).toBeInTheDocument();
  84. expect(dataLocation2Button).toBeInTheDocument();
  85. await user.click(dataFlowButton);
  86. // Colapsing flow
  87. expect(dataLocation1Button).not.toBeInTheDocument();
  88. expect(dataLocation2Button).not.toBeInTheDocument();
  89. await user.click(exectionFlowButton);
  90. expect(screen.getByRole('link', { name: '1 Execution location 1' })).toBeInTheDocument();
  91. expect(screen.getByRole('link', { name: '2 Execution location 2' })).toBeInTheDocument();
  92. expect(screen.getByRole('link', { name: '3 Execution location 3' })).toBeInTheDocument();
  93. // Keyboard interaction
  94. await user.click(dataFlowButton);
  95. dataLocation1Button = screen.getByRole('link', { name: '1 Data location 1' });
  96. dataLocation2Button = screen.getByRole('link', { name: '2 Data location 2' });
  97. // Location navigation
  98. await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
  99. expect(dataLocation1Button).toHaveAttribute('aria-current', 'true');
  100. await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
  101. expect(dataLocation1Button).toHaveAttribute('aria-current', 'false');
  102. expect(dataLocation2Button).toHaveAttribute('aria-current', 'true');
  103. await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
  104. expect(dataLocation1Button).toHaveAttribute('aria-current', 'false');
  105. expect(dataLocation2Button).toHaveAttribute('aria-current', 'false');
  106. await user.keyboard('{Alt>}{ArrowUp}{/Alt}');
  107. expect(dataLocation1Button).toHaveAttribute('aria-current', 'false');
  108. expect(dataLocation2Button).toHaveAttribute('aria-current', 'true');
  109. // Flow navigation
  110. await user.keyboard('{Alt>}{ArrowRight}{/Alt}');
  111. expect(screen.getByRole('link', { name: '1 Execution location 1' })).toHaveAttribute(
  112. 'aria-current',
  113. 'true',
  114. );
  115. await user.keyboard('{Alt>}{ArrowLeft}{/Alt}');
  116. expect(screen.getByRole('link', { name: '1 Data location 1' })).toHaveAttribute(
  117. 'aria-current',
  118. 'true',
  119. );
  120. });
  121. it('should show education principles', async () => {
  122. const user = userEvent.setup();
  123. renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject');
  124. await user.click(
  125. await screen.findByRole('tab', { name: `coding_rules.description_section.title.more_info` }),
  126. );
  127. expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument();
  128. });
  129. it('should be able to perform action on issues', async () => {
  130. const user = userEvent.setup();
  131. issuesHandler.setIsAdmin(true);
  132. renderIssueApp();
  133. // Get a specific issue list item
  134. const listItem = within(await screen.findByRole('region', { name: 'Fix that' }));
  135. expect(listItem.getByText('issue.issue_status.OPEN')).toBeInTheDocument();
  136. await act(async () => {
  137. await user.click(listItem.getByText('issue.issue_status.OPEN'));
  138. });
  139. expect(listItem.getByText('issue.transition.accept')).toBeInTheDocument();
  140. expect(listItem.getByText('issue.transition.confirm')).toBeInTheDocument();
  141. await act(async () => {
  142. await user.click(listItem.getByText('issue.transition.confirm'));
  143. });
  144. expect(listItem.getByRole('textbox')).toBeInTheDocument();
  145. await act(async () => {
  146. await user.type(listItem.getByRole('textbox'), 'test');
  147. await user.click(listItem.getByText('resolve'));
  148. });
  149. expect(
  150. listItem.getByLabelText(
  151. 'issue.transition.status_x_click_to_change.issue.issue_status.CONFIRMED',
  152. ),
  153. ).toBeInTheDocument();
  154. // Change status again
  155. await act(async () => {
  156. await user.click(listItem.getByText('issue.issue_status.CONFIRMED'));
  157. await user.click(listItem.getByText('issue.transition.accept'));
  158. await user.click(listItem.getByText('resolve'));
  159. });
  160. expect(
  161. listItem.getByLabelText(
  162. 'issue.transition.status_x_click_to_change.issue.issue_status.ACCEPTED',
  163. ),
  164. ).toBeInTheDocument();
  165. // Assign issue to a different user
  166. await act(async () => {
  167. await user.click(
  168. listItem.getByRole('combobox', { name: 'issue.assign.unassigned_click_to_assign' }),
  169. );
  170. await user.click(screen.getByLabelText('search.search_for_users'));
  171. await user.keyboard('luke');
  172. });
  173. expect(screen.getByText('Skywalker')).toBeInTheDocument();
  174. await act(async () => {
  175. await user.click(screen.getByText('Skywalker'));
  176. });
  177. await listItem.findByRole('combobox', {
  178. name: 'issue.assign.assigned_to_x_click_to_change.luke',
  179. });
  180. expect(
  181. listItem.getByRole('combobox', {
  182. name: 'issue.assign.assigned_to_x_click_to_change.luke',
  183. }),
  184. ).toBeInTheDocument();
  185. // Change tags
  186. expect(listItem.getByText('issue.no_tag')).toBeInTheDocument();
  187. await act(async () => {
  188. await user.click(listItem.getByText('issue.no_tag'));
  189. });
  190. expect(listItem.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
  191. expect(listItem.getByText('android')).toBeInTheDocument();
  192. expect(listItem.getByText('accessibility')).toBeInTheDocument();
  193. await act(async () => {
  194. await user.click(listItem.getByText('accessibility'));
  195. await user.click(listItem.getByText('android'));
  196. });
  197. await user.keyboard('{Escape}');
  198. await expect(
  199. byRole('button', { name: 'accessibility android +' }).byText('accessibility').get(),
  200. ).toHaveATooltipWithContent('accessibility, android');
  201. await act(async () => {
  202. await user.click(listItem.getByRole('button', { name: 'accessibility android +' }));
  203. });
  204. // Unselect
  205. await act(async () => {
  206. await user.click(screen.getByRole('checkbox', { name: 'accessibility' }));
  207. });
  208. await user.keyboard('{Escape}');
  209. await expect(
  210. byRole('button', { name: 'android +' }).byText('android').get(),
  211. ).toHaveATooltipWithContent('android');
  212. await act(async () => {
  213. await user.click(listItem.getByRole('button', { name: 'android +' }));
  214. });
  215. await act(async () => {
  216. await user.click(screen.getByRole('searchbox', { name: 'search.search_for_tags' }));
  217. await user.keyboard('addNewTag');
  218. });
  219. expect(
  220. screen.getByRole('checkbox', { name: 'issue.create_tag: addnewtag' }),
  221. ).toBeInTheDocument();
  222. });
  223. it('should not allow performing actions when user does not have permission', async () => {
  224. const user = userEvent.setup();
  225. renderIssueApp();
  226. await act(async () => {
  227. await user.click(await ui.issueItem4.find());
  228. });
  229. expect(
  230. screen.queryByRole('button', {
  231. name: `issue.assign.unassigned_click_to_assign`,
  232. }),
  233. ).not.toBeInTheDocument();
  234. expect(
  235. screen.queryByRole('button', {
  236. name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
  237. }),
  238. ).not.toBeInTheDocument();
  239. expect(
  240. screen.queryByRole('button', {
  241. name: `issue.transition.status_x_click_to_change.issue.status.OPEN`,
  242. }),
  243. ).not.toBeInTheDocument();
  244. expect(
  245. screen.queryByRole('button', {
  246. name: `issue.severity.severity_x_click_to_change.severity.MAJOR`,
  247. }),
  248. ).not.toBeInTheDocument();
  249. });
  250. it('should open the actions popup using keyboard shortcut', async () => {
  251. const user = userEvent.setup();
  252. issuesHandler.setIsAdmin(true);
  253. renderIssueApp();
  254. // Select an issue with an advanced rule
  255. await act(async () => {
  256. await user.click(await ui.issueItemAction5.find());
  257. // Open status popup on key press 'f'
  258. await user.keyboard('f');
  259. });
  260. expect(screen.getByText('issue.transition.confirm')).toBeInTheDocument();
  261. expect(screen.getByText('issue.transition.resolve')).toBeInTheDocument();
  262. // Open comment popup on key press 'c'
  263. await act(async () => {
  264. await user.keyboard('c');
  265. });
  266. expect(screen.getByText('issue.comment.formlink')).toBeInTheDocument();
  267. await act(async () => {
  268. await user.keyboard('{Escape}');
  269. });
  270. // Open tags popup on key press 't'
  271. await act(async () => {
  272. await user.keyboard('t');
  273. });
  274. expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
  275. expect(screen.getByText('android')).toBeInTheDocument();
  276. expect(screen.getByText('accessibility')).toBeInTheDocument();
  277. // Close tags popup
  278. await act(async () => {
  279. await user.click(screen.getByText('issue.no_tag'));
  280. // Open assign popup on key press 'a'
  281. await user.keyboard('a');
  282. });
  283. expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
  284. });
  285. it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => {
  286. localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'false');
  287. const user = userEvent.setup();
  288. issuesHandler.setIsAdmin(true);
  289. renderIssueApp();
  290. // Select an issue with an advanced rule
  291. await act(async () => {
  292. await user.click(await ui.issueItem5.find());
  293. });
  294. // open status popup on key press 'f'
  295. await user.keyboard('f');
  296. expect(screen.queryByText('issue.transition.confirm')).not.toBeInTheDocument();
  297. expect(screen.queryByText('issue.transition.resolve')).not.toBeInTheDocument();
  298. // open comment popup on key press 'c'
  299. await user.keyboard('c');
  300. expect(screen.queryByText('issue.comment.submit')).not.toBeInTheDocument();
  301. localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'true');
  302. });
  303. it('should show code tabs when any secondary location is selected', async () => {
  304. const user = userEvent.setup();
  305. renderIssueApp();
  306. await act(async () => {
  307. await user.click(await ui.issueItemAction4.find());
  308. });
  309. expect(screen.getByRole('link', { name: 'location 1' })).toBeInTheDocument();
  310. expect(screen.getByRole('link', { name: 'location 2' })).toBeInTheDocument();
  311. // Select the "why is this an issue" tab
  312. await act(async () => {
  313. await user.click(
  314. screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' }),
  315. );
  316. });
  317. expect(
  318. screen.queryByRole('tab', {
  319. name: `issue.tabs.${TabKeys.Code}`,
  320. }),
  321. ).toHaveAttribute('aria-current', 'false');
  322. await act(async () => {
  323. await user.click(screen.getByRole('link', { name: 'location 1' }));
  324. });
  325. expect(
  326. screen.queryByRole('tab', {
  327. name: `issue.tabs.${TabKeys.Code}`,
  328. }),
  329. ).toHaveAttribute('aria-current', 'true');
  330. // Select the same selected hotspot location should also navigate back to code page
  331. await act(async () => {
  332. await user.click(
  333. screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' }),
  334. );
  335. });
  336. expect(
  337. screen.queryByRole('tab', {
  338. name: `issue.tabs.${TabKeys.Code}`,
  339. }),
  340. ).toHaveAttribute('aria-current', 'false');
  341. await act(async () => {
  342. await user.click(screen.getByRole('link', { name: 'location 1' }));
  343. });
  344. expect(
  345. screen.queryByRole('tab', {
  346. name: `issue.tabs.${TabKeys.Code}`,
  347. }),
  348. ).toHaveAttribute('aria-current', 'true');
  349. });
  350. it('should show issue tags if applicable', async () => {
  351. const user = userEvent.setup();
  352. issuesHandler.setIsAdmin(true);
  353. renderIssueApp();
  354. // Select an issue with an advanced rule
  355. await act(async () => {
  356. await user.click(await ui.issueItemAction7.find());
  357. });
  358. await expect(
  359. screen.getByText('issue.quick_fix_available_with_sonarlint_no_link'),
  360. ).toHaveATooltipWithContent('issue.quick_fix_available_with_sonarlint');
  361. });
  362. });