Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

ActivityGraph-it.tsx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 { times } from 'lodash';
  23. import * as React from 'react';
  24. import { parseDate } from '../../../helpers/dates';
  25. import { mockHistoryItem, mockMeasureHistory } from '../../../helpers/mocks/project-activity';
  26. import { mockMetric } from '../../../helpers/testMocks';
  27. import { renderComponent } from '../../../helpers/testReactTestingUtils';
  28. import { byLabelText, byPlaceholderText, byRole, byText } from '../../../helpers/testSelector';
  29. import { ComponentPropsType } from '../../../helpers/testUtils';
  30. import { MetricKey, MetricType } from '../../../types/metrics';
  31. import { GraphType, MeasureHistory } from '../../../types/project-activity';
  32. import { Metric } from '../../../types/types';
  33. import GraphsHeader from '../GraphsHeader';
  34. import GraphsHistory from '../GraphsHistory';
  35. import { generateSeries, getDisplayedHistoryMetrics, splitSeriesInGraphs } from '../utils';
  36. const MAX_GRAPHS = 2;
  37. const MAX_SERIES_PER_GRAPH = 3;
  38. const HISTORY_COUNT = 10;
  39. const START_DATE = '2016-01-01T00:00:00+0200';
  40. describe('rendering', () => {
  41. it('should render correctly when loading', async () => {
  42. renderActivityGraph({ loading: true });
  43. expect(await screen.findByText('loading')).toBeInTheDocument();
  44. });
  45. it('should show the correct legend items', async () => {
  46. const { ui, user } = getPageObject();
  47. renderActivityGraph();
  48. // Static legend items, which aren't interactive.
  49. expect(ui.legendRemoveMetricBtn(MetricKey.violations).query()).not.toBeInTheDocument();
  50. expect(ui.getLegendItem(MetricKey.violations)).toBeInTheDocument();
  51. // Switch to custom graph.
  52. await ui.changeGraphType(GraphType.custom);
  53. await ui.openAddMetrics();
  54. await ui.clickOnMetric(MetricKey.bugs);
  55. await ui.clickOnMetric(MetricKey.test_failures);
  56. await user.keyboard('{Escape}');
  57. // These legend items are interactive (interaction tested below).
  58. expect(ui.legendRemoveMetricBtn(MetricKey.bugs).get()).toBeInTheDocument();
  59. expect(ui.legendRemoveMetricBtn(MetricKey.test_failures).get()).toBeInTheDocument();
  60. // Shows warning for metrics with no data.
  61. const li = ui.getLegendItem(MetricKey.test_failures);
  62. // eslint-disable-next-line jest/no-conditional-in-test
  63. if (li) {
  64. li.focus();
  65. }
  66. expect(ui.noDataWarningTooltip.get()).toBeInTheDocument();
  67. });
  68. });
  69. describe('data table modal', () => {
  70. it('shows the same data in a table', async () => {
  71. const { ui } = getPageObject();
  72. renderActivityGraph();
  73. await ui.openDataTable();
  74. expect(ui.dataTable.get()).toBeInTheDocument();
  75. expect(ui.dataTableColHeaders.getAll()).toHaveLength(3);
  76. expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
  77. // Change graph type and dates, check table updates correctly.
  78. await ui.closeDataTable();
  79. await ui.changeGraphType(GraphType.coverage);
  80. await ui.openDataTable();
  81. expect(ui.dataTable.get()).toBeInTheDocument();
  82. expect(ui.dataTableColHeaders.getAll()).toHaveLength(4);
  83. expect(ui.dataTableRows.getAll()).toHaveLength(HISTORY_COUNT + 1);
  84. });
  85. it('shows the same data in a table when filtered by date', async () => {
  86. const { ui } = getPageObject();
  87. renderActivityGraph({
  88. graphStartDate: parseDate('2017-01-01'),
  89. graphEndDate: parseDate('2019-01-01'),
  90. });
  91. await ui.openDataTable();
  92. expect(ui.dataTable.get()).toBeInTheDocument();
  93. expect(ui.dataTableColHeaders.getAll()).toHaveLength(3);
  94. expect(ui.dataTableRows.getAll()).toHaveLength(2);
  95. });
  96. });
  97. it('should correctly handle adding/removing custom metrics', async () => {
  98. const { ui } = getPageObject();
  99. renderActivityGraph();
  100. // Change graph type to "Custom".
  101. await ui.changeGraphType(GraphType.custom);
  102. // Open the "Add metrics" dropdown button; select some metrics.
  103. await ui.openAddMetrics();
  104. // We should not see DATA type or New Code metrics.
  105. expect(ui.newBugsCheckbox.query()).not.toBeInTheDocument();
  106. expect(ui.burnedBudgetCheckbox.query()).not.toBeInTheDocument();
  107. // Select 3 Int types.
  108. await ui.clickOnMetric(MetricKey.bugs);
  109. await ui.clickOnMetric(MetricKey.code_smells);
  110. await ui.clickOnMetric(MetricKey.confirmed_issues);
  111. // Select 1 Percent type.
  112. await ui.clickOnMetric(MetricKey.coverage);
  113. // We should see 2 graphs, correctly labelled.
  114. expect(ui.graphs.getAll()).toHaveLength(2);
  115. // old types and confirmed metrics should be deprecated and show a badge (both in dropdown and in legend)
  116. expect(ui.deprecatedBadge.getAll()).toHaveLength(6);
  117. // We cannot select anymore Int types. It should hide options, and show an alert.
  118. expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument();
  119. expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument();
  120. // Select 2 more Percent types.
  121. await ui.clickOnMetric(MetricKey.duplicated_lines_density);
  122. await ui.clickOnMetric(MetricKey.test_success_density);
  123. // We cannot select anymore options. It should disable all remaining options, and
  124. // show a different alert.
  125. expect(ui.maxOptionsAlert.get()).toBeInTheDocument();
  126. expect(ui.vulnerabilityCheckbox.get()).toBeDisabled();
  127. // Disable a few options.
  128. await ui.clickOnMetric(MetricKey.bugs);
  129. await ui.clickOnMetric(MetricKey.code_smells);
  130. await ui.clickOnMetric(MetricKey.coverage);
  131. // Search for option.
  132. await ui.searchForMetric('bug');
  133. expect(ui.bugsCheckbox.get()).toBeInTheDocument();
  134. expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument();
  135. // Disable final metrics by clicking on the legend items.
  136. await ui.removeMetric(MetricKey.confirmed_issues);
  137. await ui.removeMetric(MetricKey.duplicated_lines_density);
  138. await ui.removeMetric(MetricKey.test_success_density);
  139. // Should show message that there's no data to be rendered.
  140. expect(ui.noDataText.get()).toBeInTheDocument();
  141. });
  142. function getPageObject() {
  143. const user = userEvent.setup();
  144. const ui = {
  145. // Graph types.
  146. graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),
  147. // Add/remove metrics.
  148. addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
  149. deprecatedBadge: byText('deprecated'),
  150. bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }),
  151. newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }),
  152. burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }),
  153. vulnerabilityCheckbox: byRole('checkbox', { name: MetricKey.vulnerabilities }),
  154. hiddenOptionsAlert: byText('project_activity.graphs.custom.type_x_message', {
  155. exact: false,
  156. }),
  157. maxOptionsAlert: byText('project_activity.graphs.custom.add_metric_info'),
  158. filterMetrics: byPlaceholderText('search.search_for_metrics'),
  159. legendRemoveMetricBtn: (key: string) =>
  160. byRole('button', { name: `project_activity.graphs.custom.remove_metric.${key}` }),
  161. getLegendItem: (name: string) => {
  162. // This is due to a limitation in testing library, where we cannot get a listitem
  163. // role element by name.
  164. return screen.getAllByRole('listitem').find((item) => item.textContent === name);
  165. },
  166. noDataWarningTooltip: byLabelText('project_activity.graphs.custom.metric_no_history'),
  167. // Graphs.
  168. graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
  169. noDataText: byText('project_activity.graphs.custom.no_history'),
  170. // Data in table.
  171. openInTableBtn: byRole('button', { name: 'project_activity.graphs.open_in_table' }),
  172. closeDataTableBtn: byRole('button', { name: 'close' }),
  173. dataTable: byRole('table'),
  174. dataTableRows: byRole('row'),
  175. dataTableColHeaders: byRole('columnheader'),
  176. noDataTableText: byText('project_activity.graphs.data_table.no_data_warning_check_dates_x', {
  177. exact: false,
  178. }),
  179. };
  180. return {
  181. user,
  182. ui: {
  183. ...ui,
  184. async changeGraphType(type: GraphType) {
  185. await user.click(ui.graphTypeSelect.get());
  186. const optionForType = await screen.findByText(`project_activity.graphs.${type}`);
  187. await user.click(optionForType);
  188. },
  189. async openAddMetrics() {
  190. await user.click(ui.addMetricBtn.get());
  191. },
  192. async searchForMetric(text: string) {
  193. await user.type(ui.filterMetrics.get(), text);
  194. },
  195. async clickOnMetric(name: MetricKey) {
  196. await user.click(screen.getByRole('checkbox', { name }));
  197. },
  198. async removeMetric(metric: MetricKey) {
  199. await user.click(ui.legendRemoveMetricBtn(metric).get());
  200. },
  201. async openDataTable() {
  202. await user.click(ui.openInTableBtn.get());
  203. },
  204. async closeDataTable() {
  205. await user.click(ui.closeDataTableBtn.get());
  206. },
  207. },
  208. };
  209. }
  210. function renderActivityGraph(
  211. graphsHistoryProps: Partial<GraphsHistory['props']> = {},
  212. graphsHeaderProps: Partial<ComponentPropsType<typeof GraphsHeader>> = {},
  213. ) {
  214. function ActivityGraph() {
  215. const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]);
  216. const [graph, setGraph] = React.useState(graphsHistoryProps.graph || GraphType.issues);
  217. const measuresHistory: MeasureHistory[] = [];
  218. const metrics: Metric[] = [];
  219. [
  220. MetricKey.violations,
  221. MetricKey.bugs,
  222. MetricKey.code_smells,
  223. MetricKey.confirmed_issues,
  224. MetricKey.vulnerabilities,
  225. MetricKey.blocker_violations,
  226. MetricKey.lines_to_cover,
  227. MetricKey.uncovered_lines,
  228. MetricKey.coverage,
  229. MetricKey.duplicated_lines_density,
  230. MetricKey.test_success_density,
  231. ].forEach((metric) => {
  232. const history = times(HISTORY_COUNT - 2, (i) => {
  233. const date = parseDate(START_DATE);
  234. date.setDate(date.getDate() + i);
  235. return mockHistoryItem({ date, value: i.toString() });
  236. });
  237. history.push(
  238. mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200') }),
  239. mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200') }),
  240. );
  241. measuresHistory.push(mockMeasureHistory({ metric, history }));
  242. metrics.push(
  243. mockMetric({
  244. key: metric,
  245. type:
  246. metric.includes('_density') || metric === MetricKey.coverage
  247. ? MetricType.Percent
  248. : MetricType.Integer,
  249. }),
  250. );
  251. });
  252. // The following should be filtered out, and not be suggested as options.
  253. metrics.push(
  254. mockMetric({ key: MetricKey.new_bugs, type: MetricType.Integer }),
  255. mockMetric({ key: MetricKey.burned_budget, type: MetricType.Data }),
  256. );
  257. // The following will not be filtered out, but has no values.
  258. metrics.push(mockMetric({ key: MetricKey.test_failures, type: MetricType.Integer }));
  259. measuresHistory.push(
  260. mockMeasureHistory({
  261. metric: MetricKey.test_failures,
  262. history: times(HISTORY_COUNT, (i) => {
  263. const date = parseDate(START_DATE);
  264. date.setDate(date.getDate() + i);
  265. return mockHistoryItem({ date, value: undefined });
  266. }),
  267. }),
  268. );
  269. const series = generateSeries(
  270. measuresHistory,
  271. graph,
  272. metrics,
  273. getDisplayedHistoryMetrics(graph, selectedMetrics),
  274. );
  275. const graphs = splitSeriesInGraphs(series, MAX_GRAPHS, MAX_SERIES_PER_GRAPH);
  276. const metricsTypeFilter =
  277. graphs.length < MAX_GRAPHS
  278. ? undefined
  279. : graphs
  280. .filter((graph) => graph.length < MAX_SERIES_PER_GRAPH)
  281. .map((graph) => graph[0].type);
  282. const addCustomMetric = (metricKey: string) => {
  283. setSelectedMetrics([...selectedMetrics, metricKey]);
  284. };
  285. const removeCustomMetric = (metricKey: string) => {
  286. setSelectedMetrics(selectedMetrics.filter((m) => m !== metricKey));
  287. };
  288. const updateGraph = (graphType: string) => {
  289. setGraph(graphType as GraphType);
  290. };
  291. return (
  292. <>
  293. <GraphsHeader
  294. onAddCustomMetric={addCustomMetric}
  295. graph={graph}
  296. metrics={metrics}
  297. metricsTypeFilter={metricsTypeFilter}
  298. onRemoveCustomMetric={removeCustomMetric}
  299. selectedMetrics={selectedMetrics}
  300. onUpdateGraph={updateGraph}
  301. {...graphsHeaderProps}
  302. />
  303. <GraphsHistory
  304. analyses={[]}
  305. graph={graph}
  306. graphs={graphs}
  307. loading={false}
  308. measuresHistory={[]}
  309. removeCustomMetric={removeCustomMetric}
  310. series={series}
  311. {...graphsHistoryProps}
  312. />
  313. </>
  314. );
  315. }
  316. return renderComponent(<ActivityGraph />);
  317. }