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.

ProjectActivityApp-it.tsx 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  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 { keyBy, times } from 'lodash';
  23. import React from 'react';
  24. import { Route } from 'react-router-dom';
  25. import ApplicationServiceMock from '../../../../api/mocks/ApplicationServiceMock';
  26. import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
  27. import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
  28. import { mockBranchList } from '../../../../api/mocks/data/branches';
  29. import { parseDate } from '../../../../helpers/dates';
  30. import { mockComponent } from '../../../../helpers/mocks/component';
  31. import {
  32. mockAnalysis,
  33. mockAnalysisEvent,
  34. mockHistoryItem,
  35. mockMeasureHistory,
  36. } from '../../../../helpers/mocks/project-activity';
  37. import { get } from '../../../../helpers/storage';
  38. import { mockMetric } from '../../../../helpers/testMocks';
  39. import { renderAppWithComponentContext } from '../../../../helpers/testReactTestingUtils';
  40. import { byLabelText, byRole, byTestId, byText } from '../../../../helpers/testSelector';
  41. import { ComponentQualifier } from '../../../../types/component';
  42. import { MetricKey, MetricType } from '../../../../types/metrics';
  43. import {
  44. ApplicationAnalysisEventCategory,
  45. GraphType,
  46. ProjectAnalysisEventCategory,
  47. } from '../../../../types/project-activity';
  48. import ProjectActivityAppContainer from '../ProjectActivityApp';
  49. jest.mock('../../../../api/projectActivity');
  50. jest.mock('../../../../api/time-machine');
  51. jest.mock('../../../../helpers/storage', () => ({
  52. ...jest.requireActual('../../../../helpers/storage'),
  53. get: jest.fn(),
  54. save: jest.fn(),
  55. }));
  56. jest.mock('../../../../api/branches', () => ({
  57. getBranches: () => {
  58. isBranchReady = true;
  59. return Promise.resolve(mockBranchList());
  60. },
  61. }));
  62. const applicationHandler = new ApplicationServiceMock();
  63. const projectActivityHandler = new ProjectActivityServiceMock();
  64. const timeMachineHandler = new TimeMachineServiceMock();
  65. let isBranchReady = false;
  66. beforeEach(() => {
  67. isBranchReady = false;
  68. jest.clearAllMocks();
  69. applicationHandler.reset();
  70. projectActivityHandler.reset();
  71. timeMachineHandler.reset();
  72. timeMachineHandler.setMeasureHistory(
  73. [
  74. MetricKey.violations,
  75. MetricKey.bugs,
  76. MetricKey.reliability_rating,
  77. MetricKey.code_smells,
  78. MetricKey.sqale_rating,
  79. MetricKey.security_hotspots_reviewed,
  80. MetricKey.security_review_rating,
  81. MetricKey.maintainability_issues,
  82. ].map((metric) =>
  83. mockMeasureHistory({
  84. metric,
  85. history: projectActivityHandler.getAnalysesList().map(({ date }) =>
  86. mockHistoryItem({
  87. value: '3',
  88. date: parseDate(date),
  89. }),
  90. ),
  91. }),
  92. ),
  93. );
  94. });
  95. describe('rendering', () => {
  96. it('should render issues as default graph', async () => {
  97. const { ui } = getPageObject();
  98. renderProjectActivityAppContainer();
  99. await ui.appLoaded();
  100. expect(ui.graphTypeIssues.get()).toBeInTheDocument();
  101. });
  102. it('should render new code legend for applications', async () => {
  103. const { ui } = getPageObject();
  104. renderProjectActivityAppContainer(
  105. mockComponent({
  106. qualifier: ComponentQualifier.Application,
  107. breadcrumbs: [
  108. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Application },
  109. ],
  110. }),
  111. );
  112. await ui.appLoaded();
  113. expect(ui.newCodeLegend.get()).toBeInTheDocument();
  114. });
  115. it('should render new code legend for projects', async () => {
  116. const { ui } = getPageObject();
  117. renderProjectActivityAppContainer(
  118. mockComponent({
  119. qualifier: ComponentQualifier.Project,
  120. breadcrumbs: [
  121. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  122. ],
  123. leakPeriodDate: parseDate('2017-03-01T22:00:00.000Z').toDateString(),
  124. }),
  125. );
  126. await ui.appLoaded();
  127. expect(ui.newCodeLegend.get()).toBeInTheDocument();
  128. });
  129. it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])(
  130. 'should not render new code legend for %s',
  131. async (qualifier) => {
  132. const { ui } = getPageObject();
  133. renderProjectActivityAppContainer(
  134. mockComponent({
  135. qualifier,
  136. breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }],
  137. }),
  138. );
  139. await ui.appLoaded({ doNotWaitForBranch: true });
  140. expect(ui.newCodeLegend.query()).not.toBeInTheDocument();
  141. },
  142. );
  143. it('should correctly show the baseline marker', async () => {
  144. const { ui } = getPageObject();
  145. renderProjectActivityAppContainer(
  146. mockComponent({
  147. leakPeriodDate: parseDate('2017-03-01T22:00:00.000Z').toDateString(),
  148. breadcrumbs: [
  149. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  150. ],
  151. }),
  152. );
  153. await ui.appLoaded();
  154. expect(ui.baseline.get()).toBeInTheDocument();
  155. });
  156. it('should correctly show the baseline marker when first new code analysis is not present but baseline analysis is present', async () => {
  157. const { ui } = getPageObject();
  158. renderProjectActivityAppContainer(
  159. mockComponent({
  160. leakPeriodDate: parseDate('2017-03-03T22:00:00.000Z').toDateString(),
  161. breadcrumbs: [
  162. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  163. ],
  164. }),
  165. );
  166. await ui.appLoaded();
  167. expect(ui.baseline.get()).toBeInTheDocument();
  168. });
  169. it('should not show the baseline marker when first new code analysis and baseline analysis is not present', async () => {
  170. const { ui } = getPageObject();
  171. renderProjectActivityAppContainer(
  172. mockComponent({
  173. leakPeriodDate: parseDate('2017-03-10T22:00:00.000Z').toDateString(),
  174. breadcrumbs: [
  175. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  176. ],
  177. }),
  178. );
  179. await ui.appLoaded();
  180. expect(ui.baseline.query()).not.toBeInTheDocument();
  181. });
  182. it('should only show certain security hotspot-related metrics for a project', async () => {
  183. const { ui } = getPageObject();
  184. renderProjectActivityAppContainer(
  185. mockComponent({
  186. breadcrumbs: [
  187. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  188. ],
  189. }),
  190. );
  191. await ui.changeGraphType(GraphType.custom);
  192. await ui.openMetricsDropdown();
  193. expect(ui.metricCheckbox(MetricKey.security_hotspots_reviewed).get()).toBeInTheDocument();
  194. expect(ui.metricCheckbox(MetricKey.security_review_rating).query()).not.toBeInTheDocument();
  195. });
  196. it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])(
  197. 'should only show certain security hotspot-related metrics for a %s',
  198. async (qualifier) => {
  199. const { ui } = getPageObject();
  200. renderProjectActivityAppContainer(
  201. mockComponent({
  202. qualifier,
  203. breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }],
  204. }),
  205. );
  206. await ui.changeGraphType(GraphType.custom);
  207. await ui.openMetricsDropdown();
  208. expect(ui.metricCheckbox(MetricKey.security_review_rating).get()).toBeInTheDocument();
  209. expect(
  210. ui.metricCheckbox(MetricKey.security_hotspots_reviewed).query(),
  211. ).not.toBeInTheDocument();
  212. },
  213. );
  214. it('should render graph gap info message', async () => {
  215. timeMachineHandler.setMeasureHistory([
  216. mockMeasureHistory({
  217. metric: MetricKey.maintainability_issues,
  218. history: projectActivityHandler.getAnalysesList().map(({ date }, index) =>
  219. mockHistoryItem({
  220. // eslint-disable-next-line jest/no-conditional-in-test
  221. value: index === 0 ? '3' : undefined,
  222. date: parseDate(date),
  223. }),
  224. ),
  225. }),
  226. ]);
  227. const { ui } = getPageObject();
  228. renderProjectActivityAppContainer(
  229. mockComponent({
  230. breadcrumbs: [
  231. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Application },
  232. ],
  233. }),
  234. );
  235. await ui.changeGraphType(GraphType.custom);
  236. await ui.openMetricsDropdown();
  237. await ui.toggleMetric(MetricKey.maintainability_issues);
  238. expect(ui.gapInfoMessage.get()).toBeInTheDocument();
  239. });
  240. it('should not render graph gap info message if no gaps', async () => {
  241. const { ui } = getPageObject();
  242. renderProjectActivityAppContainer(
  243. mockComponent({
  244. breadcrumbs: [
  245. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Application },
  246. ],
  247. }),
  248. );
  249. await ui.changeGraphType(GraphType.custom);
  250. await ui.openMetricsDropdown();
  251. await ui.toggleMetric(MetricKey.maintainability_issues);
  252. expect(ui.gapInfoMessage.query()).not.toBeInTheDocument();
  253. });
  254. });
  255. describe('CRUD', () => {
  256. it('should correctly create, update, and delete "VERSION" events', async () => {
  257. const { ui } = getPageObject();
  258. const initialValue = '1.1-SNAPSHOT';
  259. const updatedValue = '1.1--SNAPSHOT';
  260. renderProjectActivityAppContainer(
  261. mockComponent({
  262. breadcrumbs: [
  263. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  264. ],
  265. configuration: { showHistory: true },
  266. }),
  267. );
  268. await ui.appLoaded();
  269. await ui.addVersionEvent('1.1.0.1', initialValue);
  270. expect(screen.getAllByText(initialValue).length).toBeGreaterThan(0);
  271. await ui.updateEvent(1, updatedValue);
  272. expect(screen.getAllByText(updatedValue).length).toBeGreaterThan(0);
  273. await ui.deleteEvent(0);
  274. expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
  275. });
  276. it('should correctly create, update, and delete "OTHER" events', async () => {
  277. const { ui } = getPageObject();
  278. const initialValue = 'Custom event name';
  279. const updatedValue = 'Custom event updated name';
  280. renderProjectActivityAppContainer(
  281. mockComponent({
  282. breadcrumbs: [
  283. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  284. ],
  285. configuration: { showHistory: true },
  286. }),
  287. );
  288. await ui.appLoaded();
  289. await ui.addCustomEvent('1.1.0.1', initialValue);
  290. expect(screen.getAllByText(initialValue).length).toBeGreaterThan(0);
  291. await ui.updateEvent(1, updatedValue);
  292. expect(screen.getAllByText(updatedValue).length).toBeGreaterThan(0);
  293. await ui.deleteEvent(0);
  294. expect(screen.queryByText(updatedValue)).not.toBeInTheDocument();
  295. });
  296. it('should correctly allow deletion of specific analyses', async () => {
  297. const { ui } = getPageObject();
  298. renderProjectActivityAppContainer(
  299. mockComponent({
  300. breadcrumbs: [
  301. { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project },
  302. ],
  303. configuration: { showHistory: true },
  304. }),
  305. );
  306. await ui.appLoaded();
  307. // Most recent analysis is not deletable.
  308. await ui.openCogMenu('1.1.0.2');
  309. expect(ui.deleteAnalysisBtn.query()).not.toBeInTheDocument();
  310. await ui.deleteAnalysis('1.1.0.1');
  311. expect(screen.queryByText('1.1.0.1')).not.toBeInTheDocument();
  312. });
  313. });
  314. describe('data loading', () => {
  315. function getMock(namespace: string) {
  316. // eslint-disable-next-line jest/no-conditional-in-test
  317. return namespace.includes('.custom') ? 'bugs,code_smells' : GraphType.custom;
  318. }
  319. it('should load all analyses', async () => {
  320. const count = 1000;
  321. projectActivityHandler.setAnalysesList(
  322. times(count, (i) => {
  323. return mockAnalysis({
  324. key: `analysis-${i}`,
  325. date: '2016-01-01T00:00:00+0200',
  326. });
  327. }),
  328. );
  329. const { ui } = getPageObject();
  330. renderProjectActivityAppContainer();
  331. await ui.appLoaded();
  332. expect(ui.activityItem.getAll().length).toBe(count);
  333. });
  334. it('should reload custom graph from local storage', async () => {
  335. jest.mocked(get).mockImplementationOnce(getMock).mockImplementationOnce(getMock);
  336. const { ui } = getPageObject();
  337. renderProjectActivityAppContainer();
  338. await ui.appLoaded();
  339. expect(ui.graphTypeCustom.get()).toBeInTheDocument();
  340. });
  341. it('should correctly fetch the top level component when dealing with sub portfolios', async () => {
  342. const { ui } = getPageObject();
  343. renderProjectActivityAppContainer(
  344. mockComponent({
  345. key: 'unknown',
  346. qualifier: ComponentQualifier.SubPortfolio,
  347. breadcrumbs: [
  348. { key: 'foo', name: 'foo', qualifier: ComponentQualifier.Portfolio },
  349. { key: 'unknown', name: 'unknown', qualifier: ComponentQualifier.SubPortfolio },
  350. ],
  351. }),
  352. );
  353. await ui.appLoaded({ doNotWaitForBranch: true });
  354. // If it didn't fail, it means we correctly queried for project "foo".
  355. expect(ui.activityItem.getAll().length).toBe(4);
  356. });
  357. });
  358. describe('filtering', () => {
  359. it('should correctly filter by event category', async () => {
  360. projectActivityHandler.setAnalysesList([
  361. mockAnalysis({
  362. key: `analysis-1`,
  363. events: [],
  364. }),
  365. mockAnalysis({
  366. key: `analysis-2`,
  367. events: [
  368. mockAnalysisEvent({ key: '1', category: ProjectAnalysisEventCategory.QualityGate }),
  369. ],
  370. }),
  371. mockAnalysis({
  372. key: `analysis-3`,
  373. events: [mockAnalysisEvent({ key: '2', category: ProjectAnalysisEventCategory.Version })],
  374. }),
  375. mockAnalysis({
  376. key: `analysis-4`,
  377. events: [mockAnalysisEvent({ key: '3', category: ProjectAnalysisEventCategory.Version })],
  378. }),
  379. mockAnalysis({
  380. key: `analysis-5`,
  381. events: [mockAnalysisEvent({ key: '4', category: ProjectAnalysisEventCategory.SqUpgrade })],
  382. }),
  383. mockAnalysis({
  384. key: `analysis-6`,
  385. events: [mockAnalysisEvent({ key: '5', category: ProjectAnalysisEventCategory.Version })],
  386. }),
  387. mockAnalysis({
  388. key: `analysis-7`,
  389. events: [mockAnalysisEvent({ key: '6', category: ProjectAnalysisEventCategory.SqUpgrade })],
  390. }),
  391. ]);
  392. const { ui } = getPageObject();
  393. renderProjectActivityAppContainer();
  394. await ui.appLoaded();
  395. await ui.filterByCategory(ProjectAnalysisEventCategory.Version);
  396. expect(ui.activityItem.getAll().length).toBe(3);
  397. await ui.filterByCategory(ProjectAnalysisEventCategory.QualityGate);
  398. expect(ui.activityItem.getAll().length).toBe(1);
  399. await ui.filterByCategory(ProjectAnalysisEventCategory.SqUpgrade);
  400. expect(ui.activityItem.getAll().length).toBe(2);
  401. });
  402. it('should correctly filter by date range', async () => {
  403. projectActivityHandler.setAnalysesList(
  404. times(20, (i) => {
  405. const date = parseDate('2016-01-01T00:00:00.000Z');
  406. date.setDate(date.getDate() + i);
  407. return mockAnalysis({
  408. key: `analysis-${i}`,
  409. date: date.toDateString(),
  410. });
  411. }),
  412. );
  413. const { ui } = getPageObject();
  414. renderProjectActivityAppContainer();
  415. await ui.appLoaded();
  416. expect(ui.activityItem.getAll().length).toBe(20);
  417. await ui.setDateRange('2016-01-10');
  418. expect(ui.activityItem.getAll().length).toBe(11);
  419. await ui.resetDateFilters();
  420. expect(ui.activityItem.getAll().length).toBe(20);
  421. await ui.setDateRange('2016-01-10', '2016-01-11');
  422. expect(ui.activityItem.getAll().length).toBe(2);
  423. await ui.resetDateFilters();
  424. await ui.setDateRange(undefined, '2016-01-08');
  425. expect(ui.activityItem.getAll().length).toBe(8);
  426. });
  427. });
  428. describe('graph interactions', () => {
  429. it('should allow analyses to be clicked to see details for the analysis', async () => {
  430. const { ui } = getPageObject();
  431. renderProjectActivityAppContainer();
  432. await ui.appLoaded();
  433. expect(ui.issuesPopupCell.query()).not.toBeInTheDocument();
  434. await ui.showDetails('1.1.0.1');
  435. expect(ui.issuesPopupCell.get()).toBeInTheDocument();
  436. });
  437. it('should correctly handle customizing the graph', async () => {
  438. const { ui } = getPageObject();
  439. renderProjectActivityAppContainer();
  440. await ui.appLoaded();
  441. await ui.changeGraphType(GraphType.custom);
  442. expect(ui.noDataText.get()).toBeInTheDocument();
  443. // Add metrics.
  444. await ui.openMetricsDropdown();
  445. await ui.toggleMetric(MetricKey.bugs);
  446. await ui.toggleMetric(MetricKey.security_hotspots_reviewed);
  447. await ui.closeMetricsDropdown();
  448. expect(ui.graphs.getAll()).toHaveLength(2);
  449. // Remove metrics.
  450. await ui.openMetricsDropdown();
  451. await ui.toggleMetric(MetricKey.bugs);
  452. await ui.toggleMetric(MetricKey.security_hotspots_reviewed);
  453. await ui.closeMetricsDropdown();
  454. expect(ui.noDataText.get()).toBeInTheDocument();
  455. await ui.changeGraphType(GraphType.issues);
  456. expect(ui.graphs.getAll()).toHaveLength(1);
  457. });
  458. });
  459. function getPageObject() {
  460. const user = userEvent.setup();
  461. const ui = {
  462. // Graph types.
  463. graphTypeSelect: byLabelText('project_activity.graphs.choose_type'),
  464. graphTypeIssues: byText('project_activity.graphs.issues'),
  465. graphTypeCustom: byText('project_activity.graphs.custom'),
  466. // Graphs.
  467. graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
  468. noDataText: byText('project_activity.graphs.custom.no_history'),
  469. gapInfoMessage: byText('project_activity.graphs.data_table.data_gap', { exact: false }),
  470. // Add metrics.
  471. addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
  472. metricCheckbox: (name: MetricKey) => byRole('checkbox', { name }),
  473. // Graph legend.
  474. newCodeLegend: byText('hotspot.filters.period.since_leak_period'),
  475. // Filtering.
  476. categorySelect: byLabelText('project_activity.filter_events'),
  477. resetDatesBtn: byRole('button', { name: 'project_activity.reset_dates' }),
  478. fromDateInput: byLabelText('start_date'),
  479. toDateInput: byLabelText('end_date'),
  480. // Analysis interactions.
  481. activityItem: byLabelText(/project_activity.show_analysis_X_on_graph/),
  482. cogBtn: (id: string) => byRole('button', { name: `project_activity.analysis_X_actions.${id}` }),
  483. seeDetailsBtn: (time: string) =>
  484. byLabelText(`project_activity.show_analysis_X_on_graph.${time}`),
  485. addCustomEventBtn: byRole('menuitem', { name: 'project_activity.add_custom_event' }),
  486. addVersionEvenBtn: byRole('menuitem', { name: 'project_activity.add_version' }),
  487. deleteAnalysisBtn: byRole('menuitem', { name: 'project_activity.delete_analysis' }),
  488. editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }),
  489. deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }),
  490. // Event modal.
  491. nameInput: byLabelText('name'),
  492. saveBtn: byRole('button', { name: 'save' }),
  493. changeBtn: byRole('button', { name: 'change_verb' }),
  494. deleteBtn: byRole('button', { name: 'delete' }),
  495. // Misc.
  496. loading: byText('loading'),
  497. baseline: byText('project_activity.new_code_period_start'),
  498. issuesPopupCell: byRole('cell', { name: `metric.${MetricKey.violations}.name` }),
  499. monthSelector: byTestId('month-select'),
  500. yearSelector: byTestId('year-select'),
  501. };
  502. return {
  503. user,
  504. ui: {
  505. ...ui,
  506. async appLoaded({ doNotWaitForBranch }: { doNotWaitForBranch?: boolean } = {}) {
  507. expect(await ui.graphs.findAll()).toHaveLength(1);
  508. if (!doNotWaitForBranch) {
  509. await waitFor(() => {
  510. expect(isBranchReady).toBe(true);
  511. });
  512. }
  513. },
  514. async changeGraphType(type: GraphType) {
  515. await user.click(ui.graphTypeSelect.get());
  516. const optionForType = await screen.findByText(`project_activity.graphs.${type}`);
  517. await user.click(optionForType);
  518. },
  519. async openMetricsDropdown() {
  520. await user.click(ui.addMetricBtn.get());
  521. },
  522. async toggleMetric(metric: MetricKey) {
  523. await user.click(ui.metricCheckbox(metric).get());
  524. },
  525. async closeMetricsDropdown() {
  526. await user.keyboard('{Escape}');
  527. },
  528. async openCogMenu(id: string) {
  529. await user.click(ui.cogBtn(id).get());
  530. },
  531. async deleteAnalysis(id: string) {
  532. await user.click(ui.cogBtn(id).get());
  533. await user.click(ui.deleteAnalysisBtn.get());
  534. await user.click(ui.deleteBtn.get());
  535. },
  536. async addVersionEvent(id: string, value: string) {
  537. await user.click(ui.cogBtn(id).get());
  538. await user.click(ui.addVersionEvenBtn.get());
  539. await user.type(ui.nameInput.get(), value);
  540. await user.click(ui.saveBtn.get());
  541. },
  542. async addCustomEvent(id: string, value: string) {
  543. await user.click(ui.cogBtn(id).get());
  544. await user.click(ui.addCustomEventBtn.get());
  545. await user.type(ui.nameInput.get(), value);
  546. await user.click(ui.saveBtn.get());
  547. },
  548. async updateEvent(index: number, value: string) {
  549. await user.click(ui.editEventBtn.getAll()[index]);
  550. await user.clear(ui.nameInput.get());
  551. await user.type(ui.nameInput.get(), value);
  552. await user.click(ui.changeBtn.get());
  553. },
  554. async deleteEvent(index: number) {
  555. await user.click(ui.deleteEventBtn.getAll()[index]);
  556. await user.click(ui.deleteBtn.get());
  557. },
  558. async showDetails(id: string) {
  559. await user.click(ui.seeDetailsBtn(id).get());
  560. },
  561. async filterByCategory(
  562. category: ProjectAnalysisEventCategory | ApplicationAnalysisEventCategory,
  563. ) {
  564. await user.click(ui.categorySelect.get());
  565. const optionForType = await screen.findByText(`event.category.${category}`);
  566. await user.click(optionForType);
  567. },
  568. async setDateRange(from?: string, to?: string) {
  569. if (from) {
  570. await this.selectDate(from, ui.fromDateInput.get());
  571. }
  572. if (to) {
  573. await this.selectDate(to, ui.toDateInput.get());
  574. }
  575. },
  576. async selectDate(date: string, datePickerSelector: HTMLElement) {
  577. const monthMap = [
  578. 'Jan',
  579. 'Feb',
  580. 'Mar',
  581. 'Apr',
  582. 'May',
  583. 'Jun',
  584. 'Jul',
  585. 'Aug',
  586. 'Sep',
  587. 'Oct',
  588. 'Nov',
  589. 'Dec',
  590. ];
  591. const parsedDate = parseDate(date);
  592. await user.click(datePickerSelector);
  593. const monthSelector = within(ui.monthSelector.get()).getByRole('combobox');
  594. await user.click(monthSelector);
  595. const selectedMonthElements = within(ui.monthSelector.get()).getAllByText(
  596. monthMap[parseDate(parsedDate).getMonth()],
  597. );
  598. await user.click(selectedMonthElements[selectedMonthElements.length - 1]);
  599. const yearSelector = within(ui.yearSelector.get()).getByRole('combobox');
  600. await user.click(yearSelector);
  601. const selectedYearElements = within(ui.yearSelector.get()).getAllByText(
  602. parseDate(parsedDate).getFullYear(),
  603. );
  604. await user.click(selectedYearElements[selectedYearElements.length - 1]);
  605. await user.click(
  606. screen.getByText(parseDate(parsedDate).getDate().toString(), { selector: 'button' }),
  607. );
  608. },
  609. async resetDateFilters() {
  610. await user.click(ui.resetDatesBtn.get());
  611. },
  612. },
  613. };
  614. }
  615. function renderProjectActivityAppContainer(
  616. component = mockComponent({
  617. breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }],
  618. }),
  619. ) {
  620. return renderAppWithComponentContext(
  621. `project/activity?id=${component.key}`,
  622. () => <Route path="*" element={<ProjectActivityAppContainer />} />,
  623. {
  624. metrics: keyBy(
  625. [
  626. mockMetric({ key: MetricKey.maintainability_issues, type: MetricType.Data }),
  627. mockMetric({ key: MetricKey.bugs, type: MetricType.Integer }),
  628. mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }),
  629. mockMetric({ key: MetricKey.security_hotspots_reviewed }),
  630. mockMetric({ key: MetricKey.security_review_rating, type: MetricType.Rating }),
  631. ],
  632. 'key',
  633. ),
  634. },
  635. { component },
  636. );
  637. }