Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 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 { queryHelpers, screen, within } from '@testing-library/react';
  21. import userEvent from '@testing-library/user-event';
  22. import * as React from 'react';
  23. import { SourceViewerServiceMock } from '../../../api/mocks/SourceViewerServiceMock';
  24. import { HttpStatus } from '../../../helpers/request';
  25. import { mockIssue } from '../../../helpers/testMocks';
  26. import { renderComponent } from '../../../helpers/testReactTestingUtils';
  27. import SourceViewer from '../SourceViewer';
  28. jest.mock('../../../api/components');
  29. jest.mock('../../../api/issues');
  30. jest.mock('../helpers/lines', () => {
  31. const lines = jest.requireActual('../helpers/lines');
  32. return {
  33. ...lines,
  34. LINES_TO_LOAD: 20
  35. };
  36. });
  37. const handler = new SourceViewerServiceMock();
  38. beforeEach(() => {
  39. handler.reset();
  40. });
  41. it('should show a permalink on line number', async () => {
  42. const user = userEvent.setup();
  43. renderSourceViewer();
  44. let row = await screen.findByRole('row', { name: /\/\*$/ });
  45. expect(row).toBeInTheDocument();
  46. const rowScreen = within(row);
  47. await user.click(
  48. rowScreen.getByRole('button', {
  49. name: 'source_viewer.line_X.1'
  50. })
  51. );
  52. await user.click(
  53. rowScreen.getByRole('link', {
  54. name: 'component_viewer.copy_permalink'
  55. })
  56. );
  57. expect(
  58. /* eslint-disable-next-line testing-library/prefer-presence-queries */
  59. queryHelpers.queryByAttribute(
  60. 'data-clipboard-text',
  61. row,
  62. 'http://localhost/code?id=project&selected=project%3Atest.js&line=1'
  63. )
  64. ).toBeInTheDocument();
  65. await user.keyboard('[Escape]');
  66. expect(
  67. /* eslint-disable-next-line testing-library/prefer-presence-queries */
  68. queryHelpers.queryByAttribute(
  69. 'data-clipboard-text',
  70. row,
  71. 'http://localhost/code?id=project&selected=project%3Atest.js&line=1'
  72. )
  73. ).not.toBeInTheDocument();
  74. row = await screen.findByRole('row', { name: / \* 6$/ });
  75. expect(row).toBeInTheDocument();
  76. const lowerRowScreen = within(row);
  77. await user.click(
  78. lowerRowScreen.getByRole('button', {
  79. name: 'source_viewer.line_X.6'
  80. })
  81. );
  82. expect(
  83. lowerRowScreen.getByRole('link', {
  84. name: 'component_viewer.copy_permalink'
  85. })
  86. ).toBeInTheDocument();
  87. await user.keyboard('[Escape]');
  88. });
  89. it('should show issue on empty file', async () => {
  90. renderSourceViewer({
  91. component: handler.getEmptyFile(),
  92. loadIssues: jest.fn().mockResolvedValue([
  93. mockIssue(false, {
  94. key: 'first-issue',
  95. message: 'First Issue',
  96. line: undefined,
  97. textRange: undefined
  98. })
  99. ])
  100. });
  101. expect(await screen.findByRole('table')).toBeInTheDocument();
  102. expect(await screen.findByRole('row', { name: 'First Issue' })).toBeInTheDocument();
  103. });
  104. it('should be able to interact with issue action', async () => {
  105. const user = userEvent.setup();
  106. renderSourceViewer({
  107. loadIssues: jest.fn().mockResolvedValue([
  108. mockIssue(false, {
  109. actions: ['set_type', 'set_tags', 'comment', 'set_severity', 'assign'],
  110. key: 'first-issue',
  111. message: 'First Issue',
  112. line: 1,
  113. textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
  114. })
  115. ])
  116. });
  117. //Open Issue type
  118. await user.click(
  119. await screen.findByRole('button', { name: 'issue.type.type_x_click_to_change.issue.type.BUG' })
  120. );
  121. expect(screen.getByRole('link', { name: 'issue.type.CODE_SMELL' })).toBeInTheDocument();
  122. // Open severity
  123. await user.click(
  124. await screen.findByRole('button', {
  125. name: 'issue.severity.severity_x_click_to_change.severity.MAJOR'
  126. })
  127. );
  128. expect(screen.getByRole('link', { name: 'severity.MINOR' })).toBeInTheDocument();
  129. // Close
  130. await user.keyboard('{Escape}');
  131. expect(screen.queryByRole('link', { name: 'severity.MINOR' })).not.toBeInTheDocument();
  132. // Change the severity
  133. await user.click(
  134. await screen.findByRole('button', {
  135. name: 'issue.severity.severity_x_click_to_change.severity.MAJOR'
  136. })
  137. );
  138. expect(screen.getByRole('link', { name: 'severity.MINOR' })).toBeInTheDocument();
  139. await user.click(screen.getByRole('link', { name: 'severity.MINOR' }));
  140. expect(
  141. screen.getByRole('button', {
  142. name: 'issue.severity.severity_x_click_to_change.severity.MINOR'
  143. })
  144. ).toBeInTheDocument();
  145. });
  146. it('should load line when looking arround unloaded line', async () => {
  147. const { rerender } = renderSourceViewer({
  148. aroundLine: 50,
  149. component: handler.getHugeFile()
  150. });
  151. expect(await screen.findByRole('row', { name: /Line 50$/ })).toBeInTheDocument();
  152. rerender(getSourceViewerUi({ aroundLine: 100, component: handler.getHugeFile() }));
  153. expect(await screen.findByRole('row', { name: /Line 100$/ })).toBeInTheDocument();
  154. });
  155. it('should show SCM information', async () => {
  156. const user = userEvent.setup();
  157. renderSourceViewer();
  158. let row = await screen.findByRole('row', { name: /\/\*$/ });
  159. expect(row).toBeInTheDocument();
  160. const firstRowScreen = within(row);
  161. expect(
  162. firstRowScreen.getByRole('cell', { name: 'stas.vilchik@sonarsource.com' })
  163. ).toBeInTheDocument();
  164. await user.click(
  165. firstRowScreen.getByRole('button', {
  166. name: 'source_viewer.author_X.stas.vilchik@sonarsource.com, source_viewer.click_for_scm_info'
  167. })
  168. );
  169. expect(
  170. await firstRowScreen.findByRole('heading', { level: 4, name: 'author' })
  171. ).toBeInTheDocument();
  172. expect(
  173. firstRowScreen.getByRole('heading', { level: 4, name: 'source_viewer.tooltip.scm.commited_on' })
  174. ).toBeInTheDocument();
  175. expect(
  176. firstRowScreen.getByRole('heading', { level: 4, name: 'source_viewer.tooltip.scm.revision' })
  177. ).toBeInTheDocument();
  178. row = screen.getByRole('row', { name: /\* SonarQube$/ });
  179. expect(row).toBeInTheDocument();
  180. const secondRowScreen = within(row);
  181. expect(
  182. secondRowScreen.queryByRole('cell', { name: 'stas.vilchik@sonarsource.com' })
  183. ).not.toBeInTheDocument();
  184. // SCM with no date
  185. row = await screen.findByRole('row', { name: /\* mailto:info AT sonarsource DOT com$/ });
  186. expect(row).toBeInTheDocument();
  187. const thirdRowScreen = within(row);
  188. await user.click(
  189. thirdRowScreen.getByRole('button', {
  190. name: 'source_viewer.author_X.stas.vilchik@sonarsource.com, source_viewer.click_for_scm_info'
  191. })
  192. );
  193. expect(
  194. await thirdRowScreen.findByRole('heading', { level: 4, name: 'author' })
  195. ).toBeInTheDocument();
  196. expect(
  197. thirdRowScreen.queryByRole('heading', {
  198. level: 4,
  199. name: 'source_viewer.tooltip.scm.commited_on'
  200. })
  201. ).not.toBeInTheDocument();
  202. expect(
  203. thirdRowScreen.getByRole('heading', { level: 4, name: 'source_viewer.tooltip.scm.revision' })
  204. ).toBeInTheDocument();
  205. // SCM with no date no author
  206. row = await screen.findByRole('row', { name: /\* 5$/ });
  207. expect(row).toBeInTheDocument();
  208. const fourthRowScreen = within(row);
  209. expect(fourthRowScreen.getByText('…')).toBeInTheDocument();
  210. await user.click(
  211. fourthRowScreen.getByRole('button', {
  212. name: 'source_viewer.click_for_scm_info'
  213. })
  214. );
  215. expect(
  216. fourthRowScreen.queryByRole('heading', { level: 4, name: 'author' })
  217. ).not.toBeInTheDocument();
  218. expect(
  219. fourthRowScreen.queryByRole('heading', {
  220. level: 4,
  221. name: 'source_viewer.tooltip.scm.commited_on'
  222. })
  223. ).not.toBeInTheDocument();
  224. expect(
  225. fourthRowScreen.getByRole('heading', { level: 4, name: 'source_viewer.tooltip.scm.revision' })
  226. ).toBeInTheDocument();
  227. // No SCM Popup
  228. row = await screen.findByRole('row', {
  229. name: /\* This program is free software; you can redistribute it and\/or$/
  230. });
  231. expect(row).toBeInTheDocument();
  232. expect(within(row).queryByRole('button')).not.toBeInTheDocument();
  233. });
  234. it('should show issue indicator', async () => {
  235. const user = userEvent.setup();
  236. const onIssueSelect = jest.fn();
  237. renderSourceViewer({
  238. onIssueSelect,
  239. displayAllIssues: false,
  240. loadIssues: jest.fn().mockResolvedValue([
  241. mockIssue(false, {
  242. key: 'first-issue',
  243. message: 'First Issue',
  244. line: 1,
  245. textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 1 }
  246. }),
  247. mockIssue(false, {
  248. key: 'second-issue',
  249. message: 'Second Issue',
  250. line: 1,
  251. textRange: { startLine: 1, endLine: 1, startOffset: 1, endOffset: 2 }
  252. })
  253. ])
  254. });
  255. const row = await screen.findByRole('row', { name: /.*\/ \*$/ });
  256. const issueRow = within(row);
  257. expect(issueRow.getByText('2')).toBeInTheDocument();
  258. await user.click(issueRow.getByRole('button', { name: 'source_viewer.issues_on_line.show' }));
  259. const firstIssueBox = issueRow.getByRole('region', { name: 'First Issue' });
  260. const secondIssueBox = issueRow.getByRole('region', { name: 'Second Issue' });
  261. expect(firstIssueBox).toBeInTheDocument();
  262. expect(secondIssueBox).toBeInTheDocument();
  263. await user.click(firstIssueBox);
  264. expect(onIssueSelect).toBeCalledWith('first-issue');
  265. await user.click(secondIssueBox);
  266. expect(onIssueSelect).toBeCalledWith('second-issue');
  267. });
  268. it('should show coverage information', async () => {
  269. renderSourceViewer();
  270. const coverdLine = within(
  271. await screen.findByRole('row', { name: /\* mailto:info AT sonarsource DOT com$/ })
  272. );
  273. expect(
  274. coverdLine.getByLabelText('source_viewer.tooltip.covered.conditions.1')
  275. ).toBeInTheDocument();
  276. const partialyCoveredWithConditionLine = within(
  277. await screen.findByRole('row', { name: / \* 5$/ })
  278. );
  279. expect(
  280. partialyCoveredWithConditionLine.getByLabelText(
  281. 'source_viewer.tooltip.partially-covered.conditions.1.2'
  282. )
  283. ).toBeInTheDocument();
  284. const partialyCoveredLine = within(await screen.findByRole('row', { name: /\/\*$/ }));
  285. expect(
  286. partialyCoveredLine.getByLabelText('source_viewer.tooltip.partially-covered')
  287. ).toBeInTheDocument();
  288. const uncoveredLine = within(await screen.findByRole('row', { name: / \* 6$/ }));
  289. expect(uncoveredLine.getByLabelText('source_viewer.tooltip.uncovered')).toBeInTheDocument();
  290. const uncoveredWithConditionLine = within(
  291. await screen.findByRole('row', { name: / \* SonarQube$/ })
  292. );
  293. expect(
  294. uncoveredWithConditionLine.getByLabelText('source_viewer.tooltip.uncovered.conditions.1')
  295. ).toBeInTheDocument();
  296. const coveredWithNoCondition = within(await screen.findByRole('row', { name: /\* Copyright$/ }));
  297. expect(
  298. coveredWithNoCondition.getByLabelText('source_viewer.tooltip.covered')
  299. ).toBeInTheDocument();
  300. });
  301. it('should show duplication block', async () => {
  302. const user = userEvent.setup();
  303. renderSourceViewer();
  304. const duplicateLine = within(await screen.findByRole('row', { name: /\* 7$/ }));
  305. expect(
  306. duplicateLine.getByLabelText('source_viewer.tooltip.duplicated_block')
  307. ).toBeInTheDocument();
  308. await user.click(
  309. duplicateLine.getByRole('button', { name: 'source_viewer.tooltip.duplicated_block' })
  310. );
  311. expect(duplicateLine.getAllByRole('link', { name: 'test2.js' })[0]).toBeInTheDocument();
  312. await user.keyboard('[Escape]');
  313. expect(duplicateLine.queryByRole('link', { name: 'test2.js' })).not.toBeInTheDocument();
  314. });
  315. it('should highlight symbol', async () => {
  316. const user = userEvent.setup();
  317. renderSourceViewer({ component: 'project:testSymb.tsx' });
  318. const symbols = await screen.findAllByText('symbole');
  319. await user.click(symbols[0]);
  320. // For now just check the class. Maybe found a better accessible way of showing higlighted symbole
  321. symbols.forEach(element => {
  322. expect(element).toHaveClass('highlighted');
  323. });
  324. });
  325. it('should show correct message when component is not asscessible', async () => {
  326. handler.setFailLoadingComponentStatus(HttpStatus.Forbidden);
  327. renderSourceViewer();
  328. expect(
  329. await screen.findByText('code_viewer.no_source_code_displayed_due_to_security')
  330. ).toBeInTheDocument();
  331. });
  332. it('should show correct message when component does not exist', async () => {
  333. handler.setFailLoadingComponentStatus(HttpStatus.NotFound);
  334. renderSourceViewer();
  335. expect(await screen.findByText('component_viewer.no_component')).toBeInTheDocument();
  336. });
  337. function renderSourceViewer(override?: Partial<SourceViewer['props']>) {
  338. return renderComponent(getSourceViewerUi(override));
  339. }
  340. function getSourceViewerUi(override?: Partial<SourceViewer['props']>) {
  341. return (
  342. <SourceViewer
  343. aroundLine={1}
  344. branchLike={undefined}
  345. component={handler.getFileWithSource()}
  346. displayAllIssues={true}
  347. displayIssueLocationsCount={true}
  348. displayIssueLocationsLink={false}
  349. displayLocationMarkers={true}
  350. loadIssues={jest.fn().mockResolvedValue([])}
  351. onIssueChange={jest.fn()}
  352. onIssueSelect={jest.fn()}
  353. onLoaded={jest.fn()}
  354. onLocationSelect={jest.fn()}
  355. scroll={jest.fn()}
  356. slimHeader={true}
  357. {...override}
  358. />
  359. );
  360. }