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.

Code-it.ts 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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, waitFor, within } from '@testing-library/react';
  21. import userEvent from '@testing-library/user-event';
  22. import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
  23. import { keyBy, omit, times } from 'lodash';
  24. import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
  25. import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
  26. import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
  27. import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
  28. import { isDiffMetric } from '../../../helpers/measures';
  29. import { mockComponent } from '../../../helpers/mocks/component';
  30. import { mockMeasure } from '../../../helpers/testMocks';
  31. import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
  32. import { QuerySelector, byLabelText, byRole, byText } from '../../../helpers/testSelector';
  33. import { ComponentQualifier } from '../../../types/component';
  34. import { MetricKey } from '../../../types/metrics';
  35. import { Component } from '../../../types/types';
  36. import routes from '../routes';
  37. jest.mock('../../../components/intl/DateFromNow');
  38. jest.mock('../../../components/SourceViewer/helpers/lines', () => {
  39. const lines = jest.requireActual('../../../components/SourceViewer/helpers/lines');
  40. return {
  41. ...lines,
  42. LINES_TO_LOAD: 20,
  43. };
  44. });
  45. jest.mock('../../../api/quality-gates', () => ({
  46. getQualityGateProjectStatus: jest.fn(),
  47. }));
  48. const DEFAULT_LINES_LOADED = 19;
  49. const originalScrollTo = window.scrollTo;
  50. const branchesHandler = new BranchesServiceMock();
  51. const componentsHandler = new ComponentsServiceMock();
  52. const issuesHandler = new IssuesServiceMock();
  53. beforeAll(() => {
  54. Object.defineProperty(window, 'scrollTo', {
  55. writable: true,
  56. value: () => {
  57. /* noop */
  58. },
  59. });
  60. });
  61. afterAll(() => {
  62. Object.defineProperty(window, 'scrollTo', {
  63. writable: true,
  64. value: originalScrollTo,
  65. });
  66. });
  67. beforeEach(() => {
  68. branchesHandler.reset();
  69. componentsHandler.reset();
  70. issuesHandler.reset();
  71. });
  72. it('should allow navigating through the tree', async () => {
  73. const ui = getPageObject(userEvent.setup());
  74. renderCode();
  75. await ui.appLoaded();
  76. // Navigate by clicking on an element.
  77. await ui.clickOnChildComponent(/folderA$/);
  78. expect(await ui.childComponent(/out\.tsx/).findAll()).toHaveLength(2); // One for the pin, one for the name column
  79. expect(screen.getByRole('navigation', { name: 'breadcrumbs' })).toBeInTheDocument();
  80. // Navigate back using the breadcrumb.
  81. await ui.clickOnBreadcrumb(/Foo$/);
  82. expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument();
  83. expect(screen.queryByRole('navigation', { name: 'breadcrumbs' })).not.toBeInTheDocument();
  84. // Open "index.tsx" file using keyboard navigation.
  85. await ui.arrowDown();
  86. await ui.arrowDown();
  87. await ui.arrowRight();
  88. // Load source viewer.
  89. expect((await ui.sourceCode.findAll()).length).toEqual(DEFAULT_LINES_LOADED);
  90. // Navigate back using keyboard.
  91. await ui.arrowLeft();
  92. expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument();
  93. });
  94. it('should behave correctly when using search', async () => {
  95. const ui = getPageObject(userEvent.setup());
  96. renderCode({
  97. navigateTo: `code?id=foo&search=nonexistent`,
  98. });
  99. await ui.appLoaded();
  100. // Starts with a query from the URL.
  101. expect(await ui.noResultsTxt.find()).toBeInTheDocument();
  102. await ui.clearSearch();
  103. // Search with results that are deeper than the current level.
  104. await ui.searchForComponent('out');
  105. expect(ui.searchResult(/out\.tsx/).get()).toBeInTheDocument();
  106. // Search with no results.
  107. await ui.searchForComponent('nonexistent');
  108. expect(await ui.noResultsTxt.find()).toBeInTheDocument();
  109. await ui.clearSearch();
  110. // Open file using keyboard navigation.
  111. await ui.searchForComponent('index');
  112. await ui.arrowDown();
  113. await ui.arrowDown();
  114. await ui.arrowRight();
  115. // Load source viewer.
  116. expect((await ui.sourceCode.findAll()).length).toEqual(DEFAULT_LINES_LOADED);
  117. // Navigate back using keyboard.
  118. await ui.arrowLeft();
  119. expect(await ui.searchResult(/folderA/).find()).toBeInTheDocument();
  120. });
  121. it('should correcly handle long lists of components', async () => {
  122. const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
  123. componentsHandler.registerComponentTree({
  124. component,
  125. ancestors: [],
  126. children: times(300, (n) => ({
  127. component: mockComponent({
  128. key: `foo:file${n}`,
  129. name: `file${n}`,
  130. qualifier: ComponentQualifier.File,
  131. }),
  132. ancestors: [component],
  133. children: [],
  134. })),
  135. });
  136. const ui = getPageObject(userEvent.setup());
  137. renderCode();
  138. await ui.appLoaded();
  139. expect(ui.showingOutOfTxt(100, 300).get()).toBeInTheDocument();
  140. await ui.clickLoadMore();
  141. expect(ui.showingOutOfTxt(200, 300).get()).toBeInTheDocument();
  142. });
  143. it.each([
  144. ComponentQualifier.Application,
  145. ComponentQualifier.Project,
  146. ComponentQualifier.Portfolio,
  147. ComponentQualifier.SubPortfolio,
  148. ])('should render correctly when there are no child components for %s', async (qualifier) => {
  149. const component = mockComponent({
  150. ...componentsHandler.findComponentTree('foo')?.component,
  151. qualifier,
  152. canBrowseAllChildProjects: true,
  153. });
  154. componentsHandler.registerComponentTree({
  155. component,
  156. ancestors: [],
  157. children: [],
  158. });
  159. const ui = getPageObject(userEvent.setup());
  160. renderCode({ component });
  161. expect(await ui.componentIsEmptyTxt(qualifier).find()).toBeInTheDocument();
  162. });
  163. it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])(
  164. 'should render a warning when not having access to all children for %s',
  165. async (qualifier) => {
  166. const ui = getPageObject(userEvent.setup());
  167. renderCode({
  168. component: mockComponent({
  169. ...componentsHandler.findComponentTree('foo')?.component,
  170. qualifier,
  171. canBrowseAllChildProjects: false,
  172. }),
  173. });
  174. expect(await ui.notAccessToAllChildrenTxt.find()).toBeInTheDocument();
  175. },
  176. );
  177. it('should correctly show measures for a project', async () => {
  178. const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
  179. componentsHandler.registerComponentTree({
  180. component,
  181. ancestors: [],
  182. children: [
  183. {
  184. component: mockComponent({
  185. key: 'folderA',
  186. name: 'folderA',
  187. qualifier: ComponentQualifier.Directory,
  188. }),
  189. ancestors: [component],
  190. children: [],
  191. },
  192. {
  193. component: mockComponent({
  194. key: 'index.tsx',
  195. name: 'index.tsx',
  196. qualifier: ComponentQualifier.File,
  197. }),
  198. ancestors: [component],
  199. children: [],
  200. },
  201. ],
  202. });
  203. componentsHandler.registerComponentMeasures({
  204. foo: { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc }) },
  205. folderA: generateMeasures('2.0'),
  206. 'index.tsx': {},
  207. });
  208. const ui = getPageObject(userEvent.setup());
  209. renderCode();
  210. await ui.appLoaded(component.name);
  211. // Folder A
  212. const folderRow = ui.measureRow(/folderA/);
  213. [
  214. [MetricKey.ncloc, '2'],
  215. [MetricKey.security_issues, '4'],
  216. [MetricKey.reliability_issues, '4'],
  217. [MetricKey.maintainability_issues, '4'],
  218. [MetricKey.security_hotspots, '2'],
  219. [MetricKey.coverage, '2.0%'],
  220. [MetricKey.duplicated_lines_density, '2.0%'],
  221. ].forEach(([domain, value]) => {
  222. expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument();
  223. });
  224. // index.tsx
  225. const fileRow = ui.measureRow(/index\.tsx/);
  226. [
  227. [MetricKey.ncloc, '—'],
  228. [MetricKey.security_issues, '—'],
  229. [MetricKey.reliability_issues, '—'],
  230. [MetricKey.maintainability_issues, '—'],
  231. [MetricKey.security_hotspots, '—'],
  232. [MetricKey.coverage, '—'],
  233. [MetricKey.duplicated_lines_density, '—'],
  234. ].forEach(([domain, value]) => {
  235. expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument();
  236. });
  237. });
  238. it('should correctly show measures for a project when relying on old taxonomy', async () => {
  239. const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
  240. componentsHandler.registerComponentTree({
  241. component,
  242. ancestors: [],
  243. children: [
  244. {
  245. component: mockComponent({
  246. key: 'folderA',
  247. name: 'folderA',
  248. qualifier: ComponentQualifier.Directory,
  249. }),
  250. ancestors: [component],
  251. children: [],
  252. },
  253. {
  254. component: mockComponent({
  255. key: 'index.tsx',
  256. name: 'index.tsx',
  257. qualifier: ComponentQualifier.File,
  258. }),
  259. ancestors: [component],
  260. children: [],
  261. },
  262. ],
  263. });
  264. componentsHandler.registerComponentMeasures({
  265. foo: { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc }) },
  266. folderA: omit(generateMeasures('2.0'), CCT_SOFTWARE_QUALITY_METRICS),
  267. 'index.tsx': {},
  268. });
  269. const ui = getPageObject(userEvent.setup());
  270. renderCode();
  271. await ui.appLoaded(component.name);
  272. // Folder A
  273. const folderRow = ui.measureRow(/folderA/);
  274. [
  275. [MetricKey.ncloc, '2'],
  276. [MetricKey.security_issues, '2'],
  277. [MetricKey.reliability_issues, '2'],
  278. [MetricKey.maintainability_issues, '2'],
  279. [MetricKey.security_hotspots, '2'],
  280. [MetricKey.coverage, '2.0%'],
  281. [MetricKey.duplicated_lines_density, '2.0%'],
  282. ].forEach(([domain, value]) => {
  283. expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument();
  284. });
  285. // index.tsx
  286. const fileRow = ui.measureRow(/index\.tsx/);
  287. [
  288. [MetricKey.ncloc, '—'],
  289. [MetricKey.security_issues, '—'],
  290. [MetricKey.reliability_issues, '—'],
  291. [MetricKey.maintainability_issues, '—'],
  292. [MetricKey.security_hotspots, '—'],
  293. [MetricKey.coverage, '—'],
  294. [MetricKey.duplicated_lines_density, '—'],
  295. ].forEach(([domain, value]) => {
  296. expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument();
  297. });
  298. });
  299. it('should correctly show new VS overall measures for Portfolios', async () => {
  300. const component = mockComponent({
  301. key: 'portfolio',
  302. name: 'Portfolio',
  303. qualifier: ComponentQualifier.Portfolio,
  304. canBrowseAllChildProjects: true,
  305. });
  306. componentsHandler.registerComponentTree({
  307. component,
  308. ancestors: [],
  309. children: [
  310. {
  311. component: mockComponent({
  312. analysisDate: '2022-02-01',
  313. key: 'child1',
  314. name: 'Child 1',
  315. }),
  316. ancestors: [component],
  317. children: [],
  318. },
  319. {
  320. component: mockComponent({
  321. key: 'child2',
  322. name: 'Child 2',
  323. }),
  324. ancestors: [component],
  325. children: [],
  326. },
  327. ],
  328. });
  329. componentsHandler.registerComponentMeasures({
  330. portfolio: generateMeasures('1.0', '2.0'),
  331. child1: generateMeasures('2.0', '3.0'),
  332. child2: {
  333. [MetricKey.alert_status]: mockMeasure({
  334. metric: MetricKey.alert_status,
  335. value: 'ERROR',
  336. period: undefined,
  337. }),
  338. },
  339. });
  340. const ui = getPageObject(userEvent.setup());
  341. renderCode({ component });
  342. await ui.appLoaded(component.name);
  343. // New code measures.
  344. expect(ui.newCodeBtn.get()).toHaveAttribute('aria-current', 'true');
  345. // Child 1
  346. let child1Row = ui.measureRow(/^Child 1/);
  347. [
  348. ['Releasability', 'OK'],
  349. ['security', 'C'],
  350. ['Reliability', 'C'],
  351. ['Maintainability', 'C'],
  352. ['security_review', 'C'],
  353. ['ncloc', '3'],
  354. ['last_analysis_date', '2022-02-01'],
  355. ].forEach(([domain, value]) => {
  356. expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument();
  357. });
  358. // Child 2
  359. let child2Row = ui.measureRow(/^Child 2/);
  360. [
  361. ['Releasability', 'ERROR'],
  362. ['security', '—'],
  363. ['Reliability', '—'],
  364. ['Maintainability', '—'],
  365. ['security_review', '—'],
  366. ['ncloc', '—'],
  367. ['last_analysis_date', '—'],
  368. ].forEach(([domain, value]) => {
  369. expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument();
  370. });
  371. // Overall code measures
  372. await ui.showOverallCode();
  373. // Child 1
  374. child1Row = ui.measureRow(/^Child 1/);
  375. [
  376. ['Releasability', 'OK'],
  377. ['security', 'B'],
  378. ['Reliability', 'B'],
  379. ['Maintainability', 'B'],
  380. ['security_review', 'B'],
  381. ['ncloc', '2'],
  382. ].forEach(([domain, value]) => {
  383. expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument();
  384. });
  385. // Child 2
  386. child2Row = ui.measureRow(/^Child 2/);
  387. [
  388. ['Releasability', 'ERROR'],
  389. ['security', '—'],
  390. ['Reliability', '—'],
  391. ['Maintainability', '—'],
  392. ['security_review', '—'],
  393. ['ncloc', '—'],
  394. ].forEach(([domain, value]) => {
  395. expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument();
  396. });
  397. });
  398. function getPageObject(user: UserEvent) {
  399. const ui = {
  400. componentName: (name: string) => byText(name),
  401. childComponent: (name: string | RegExp) => byRole('cell', { name }),
  402. searchResult: (name: string | RegExp) => byRole('link', { name }),
  403. componentIsEmptyTxt: (qualifier: ComponentQualifier) =>
  404. byText(`code_viewer.no_source_code_displayed_due_to_empty_analysis.${qualifier}`),
  405. searchInput: byRole('searchbox'),
  406. noResultsTxt: byText('no_results'),
  407. sourceCode: byText('function Test() {}'),
  408. notAccessToAllChildrenTxt: byText('code_viewer.not_all_measures_are_shown'),
  409. showingOutOfTxt: (x: number, y: number) => byText(`x_of_y_shown.${x}.${y}`),
  410. newCodeBtn: byRole('radio', { name: 'projects.view.new_code' }),
  411. overallCodeBtn: byRole('radio', { name: 'projects.view.overall_code' }),
  412. measureRow: (name: string | RegExp) => byLabelText(name),
  413. measureValueCell: (row: QuerySelector, name: string, value: string) => {
  414. const i = Array.from(screen.getAllByRole('columnheader')).findIndex((c) =>
  415. c.textContent?.includes(name),
  416. );
  417. if (i < 0) {
  418. // eslint-disable-next-line testing-library/no-debugging-utils
  419. screen.debug(screen.getByRole('table'), 40000);
  420. throw new Error(`Couldn't locate column with header ${name}`);
  421. }
  422. const cell = row.byRole('cell').getAll().at(i);
  423. if (cell === undefined) {
  424. throw new Error(`Couldn't locate cell with value ${value} for header ${name}`);
  425. }
  426. return within(cell).getByText(value);
  427. },
  428. };
  429. return {
  430. ...ui,
  431. async searchForComponent(text: string) {
  432. await user.type(ui.searchInput.get(), text);
  433. },
  434. async clearSearch() {
  435. await user.clear(ui.searchInput.get());
  436. },
  437. async clickOnChildComponent(name: string | RegExp) {
  438. await user.click(screen.getByRole('link', { name }));
  439. },
  440. async appLoaded(name = 'Foo') {
  441. await waitFor(() => {
  442. expect(ui.componentName(name).get()).toBeInTheDocument();
  443. });
  444. },
  445. async clickOnBreadcrumb(name: string | RegExp) {
  446. await user.click(screen.getByRole('link', { name }));
  447. },
  448. async arrowDown() {
  449. await user.keyboard('[ArrowDown]');
  450. },
  451. async arrowRight() {
  452. await user.keyboard('[ArrowRight]');
  453. },
  454. async arrowLeft() {
  455. await user.keyboard('[ArrowLeft]');
  456. },
  457. async clickLoadMore() {
  458. await user.click(screen.getByRole('button', { name: 'show_more' }));
  459. },
  460. async showOverallCode() {
  461. await user.click(ui.overallCodeBtn.get());
  462. },
  463. };
  464. }
  465. function generateMeasures(overallValue = '1.0', newValue = '2.0') {
  466. return keyBy(
  467. [
  468. ...[
  469. MetricKey.security_issues,
  470. MetricKey.reliability_issues,
  471. MetricKey.maintainability_issues,
  472. ].map((metric) =>
  473. mockMeasure({ metric, value: JSON.stringify({ total: 4 }), period: undefined }),
  474. ),
  475. ...[
  476. MetricKey.ncloc,
  477. MetricKey.new_lines,
  478. MetricKey.bugs,
  479. MetricKey.new_bugs,
  480. MetricKey.vulnerabilities,
  481. MetricKey.new_vulnerabilities,
  482. MetricKey.code_smells,
  483. MetricKey.new_code_smells,
  484. MetricKey.security_hotspots,
  485. MetricKey.new_security_hotspots,
  486. MetricKey.coverage,
  487. MetricKey.new_coverage,
  488. MetricKey.duplicated_lines_density,
  489. MetricKey.new_duplicated_lines_density,
  490. MetricKey.releasability_rating,
  491. MetricKey.reliability_rating,
  492. MetricKey.new_reliability_rating,
  493. MetricKey.sqale_rating,
  494. MetricKey.new_maintainability_rating,
  495. MetricKey.security_rating,
  496. MetricKey.new_security_rating,
  497. MetricKey.security_review_rating,
  498. MetricKey.new_security_review_rating,
  499. ].map((metric) =>
  500. isDiffMetric(metric)
  501. ? mockMeasure({ metric, period: { index: 1, value: newValue } })
  502. : mockMeasure({ metric, value: overallValue, period: undefined }),
  503. ),
  504. mockMeasure({
  505. metric: MetricKey.alert_status,
  506. value: overallValue === '1.0' || overallValue === '2.0' ? 'OK' : 'ERROR',
  507. period: undefined,
  508. }),
  509. ],
  510. 'metric',
  511. );
  512. }
  513. function renderCode({
  514. component = componentsHandler.findComponentTree('foo')?.component,
  515. navigateTo,
  516. }: { component?: Component; navigateTo?: string } = {}) {
  517. return renderAppWithComponentContext('code', routes, { navigateTo }, { component });
  518. }