123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- /*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
- import { screen, waitFor, within } from '@testing-library/react';
- import userEvent from '@testing-library/user-event';
- import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
- import { keyBy, omit, times } from 'lodash';
- import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock';
- import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
- import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
- import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
- import { isDiffMetric } from '../../../helpers/measures';
- import { mockComponent } from '../../../helpers/mocks/component';
- import { mockMeasure } from '../../../helpers/testMocks';
- import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
- import { QuerySelector, byLabelText, byRole, byText } from '../../../helpers/testSelector';
- import { ComponentQualifier } from '../../../types/component';
- import { MetricKey } from '../../../types/metrics';
- import { Component } from '../../../types/types';
- import routes from '../routes';
-
- jest.mock('../../../components/intl/DateFromNow');
-
- jest.mock('../../../components/SourceViewer/helpers/lines', () => {
- const lines = jest.requireActual('../../../components/SourceViewer/helpers/lines');
- return {
- ...lines,
- LINES_TO_LOAD: 20,
- };
- });
-
- jest.mock('../../../api/quality-gates', () => ({
- getQualityGateProjectStatus: jest.fn(),
- }));
-
- const DEFAULT_LINES_LOADED = 19;
- const originalScrollTo = window.scrollTo;
-
- const branchesHandler = new BranchesServiceMock();
- const componentsHandler = new ComponentsServiceMock();
- const issuesHandler = new IssuesServiceMock();
-
- beforeAll(() => {
- Object.defineProperty(window, 'scrollTo', {
- writable: true,
- value: () => {
- /* noop */
- },
- });
- });
-
- afterAll(() => {
- Object.defineProperty(window, 'scrollTo', {
- writable: true,
- value: originalScrollTo,
- });
- });
-
- beforeEach(() => {
- branchesHandler.reset();
- componentsHandler.reset();
- issuesHandler.reset();
- });
-
- it('should allow navigating through the tree', async () => {
- const ui = getPageObject(userEvent.setup());
- renderCode();
- await ui.appLoaded();
-
- // Navigate by clicking on an element.
- await ui.clickOnChildComponent(/folderA$/);
- expect(await ui.childComponent(/out\.tsx/).findAll()).toHaveLength(2); // One for the pin, one for the name column
- expect(screen.getByRole('navigation', { name: 'breadcrumbs' })).toBeInTheDocument();
-
- // Navigate back using the breadcrumb.
- await ui.clickOnBreadcrumb(/Foo$/);
- expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument();
- expect(screen.queryByRole('navigation', { name: 'breadcrumbs' })).not.toBeInTheDocument();
-
- // Open "index.tsx" file using keyboard navigation.
- await ui.arrowDown();
- await ui.arrowDown();
- await ui.arrowRight();
- // Load source viewer.
- expect((await ui.sourceCode.findAll()).length).toEqual(DEFAULT_LINES_LOADED);
-
- // Navigate back using keyboard.
- await ui.arrowLeft();
- expect(await ui.childComponent(/folderA/).find()).toBeInTheDocument();
- });
-
- it('should behave correctly when using search', async () => {
- const ui = getPageObject(userEvent.setup());
- renderCode({
- navigateTo: `code?id=foo&search=nonexistent`,
- });
- await ui.appLoaded();
-
- // Starts with a query from the URL.
- expect(await ui.noResultsTxt.find()).toBeInTheDocument();
- await ui.clearSearch();
-
- // Search with results that are deeper than the current level.
- await ui.searchForComponent('out');
- expect(ui.searchResult(/out\.tsx/).get()).toBeInTheDocument();
-
- // Search with no results.
- await ui.searchForComponent('nonexistent');
- expect(await ui.noResultsTxt.find()).toBeInTheDocument();
- await ui.clearSearch();
-
- // Open file using keyboard navigation.
- await ui.searchForComponent('index');
- await ui.arrowDown();
- await ui.arrowDown();
- await ui.arrowRight();
- // Load source viewer.
- expect((await ui.sourceCode.findAll()).length).toEqual(DEFAULT_LINES_LOADED);
-
- // Navigate back using keyboard.
- await ui.arrowLeft();
- expect(await ui.searchResult(/folderA/).find()).toBeInTheDocument();
- });
-
- it('should correcly handle long lists of components', async () => {
- const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
- componentsHandler.registerComponentTree({
- component,
- ancestors: [],
- children: times(300, (n) => ({
- component: mockComponent({
- key: `foo:file${n}`,
- name: `file${n}`,
- qualifier: ComponentQualifier.File,
- }),
- ancestors: [component],
- children: [],
- })),
- });
- const ui = getPageObject(userEvent.setup());
- renderCode();
- await ui.appLoaded();
-
- expect(ui.showingOutOfTxt(100, 300).get()).toBeInTheDocument();
- await ui.clickLoadMore();
- expect(ui.showingOutOfTxt(200, 300).get()).toBeInTheDocument();
- });
-
- it.each([
- ComponentQualifier.Application,
- ComponentQualifier.Project,
- ComponentQualifier.Portfolio,
- ComponentQualifier.SubPortfolio,
- ])('should render correctly when there are no child components for %s', async (qualifier) => {
- const component = mockComponent({
- ...componentsHandler.findComponentTree('foo')?.component,
- qualifier,
- canBrowseAllChildProjects: true,
- });
- componentsHandler.registerComponentTree({
- component,
- ancestors: [],
- children: [],
- });
- const ui = getPageObject(userEvent.setup());
- renderCode({ component });
-
- expect(await ui.componentIsEmptyTxt(qualifier).find()).toBeInTheDocument();
- });
-
- it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])(
- 'should render a warning when not having access to all children for %s',
- async (qualifier) => {
- const ui = getPageObject(userEvent.setup());
- renderCode({
- component: mockComponent({
- ...componentsHandler.findComponentTree('foo')?.component,
- qualifier,
- canBrowseAllChildProjects: false,
- }),
- });
-
- expect(await ui.notAccessToAllChildrenTxt.find()).toBeInTheDocument();
- },
- );
-
- it('should correctly show measures for a project', async () => {
- const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
- componentsHandler.registerComponentTree({
- component,
- ancestors: [],
- children: [
- {
- component: mockComponent({
- key: 'folderA',
- name: 'folderA',
- qualifier: ComponentQualifier.Directory,
- }),
- ancestors: [component],
- children: [],
- },
- {
- component: mockComponent({
- key: 'index.tsx',
- name: 'index.tsx',
- qualifier: ComponentQualifier.File,
- }),
- ancestors: [component],
- children: [],
- },
- ],
- });
- componentsHandler.registerComponentMeasures({
- foo: { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc }) },
- folderA: generateMeasures('2.0'),
- 'index.tsx': {},
- });
- const ui = getPageObject(userEvent.setup());
- renderCode();
- await ui.appLoaded(component.name);
-
- // Folder A
- const folderRow = ui.measureRow(/folderA/);
- [
- [MetricKey.ncloc, '2'],
- [MetricKey.security_issues, '4'],
- [MetricKey.reliability_issues, '4'],
- [MetricKey.maintainability_issues, '4'],
- [MetricKey.security_hotspots, '2'],
- [MetricKey.coverage, '2.0%'],
- [MetricKey.duplicated_lines_density, '2.0%'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument();
- });
-
- // index.tsx
- const fileRow = ui.measureRow(/index\.tsx/);
- [
- [MetricKey.ncloc, '—'],
- [MetricKey.security_issues, '—'],
- [MetricKey.reliability_issues, '—'],
- [MetricKey.maintainability_issues, '—'],
- [MetricKey.security_hotspots, '—'],
- [MetricKey.coverage, '—'],
- [MetricKey.duplicated_lines_density, '—'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument();
- });
- });
-
- it('should correctly show measures for a project when relying on old taxonomy', async () => {
- const component = mockComponent(componentsHandler.findComponentTree('foo')?.component);
- componentsHandler.registerComponentTree({
- component,
- ancestors: [],
- children: [
- {
- component: mockComponent({
- key: 'folderA',
- name: 'folderA',
- qualifier: ComponentQualifier.Directory,
- }),
- ancestors: [component],
- children: [],
- },
- {
- component: mockComponent({
- key: 'index.tsx',
- name: 'index.tsx',
- qualifier: ComponentQualifier.File,
- }),
- ancestors: [component],
- children: [],
- },
- ],
- });
- componentsHandler.registerComponentMeasures({
- foo: { [MetricKey.ncloc]: mockMeasure({ metric: MetricKey.ncloc }) },
- folderA: omit(generateMeasures('2.0'), CCT_SOFTWARE_QUALITY_METRICS),
- 'index.tsx': {},
- });
- const ui = getPageObject(userEvent.setup());
- renderCode();
- await ui.appLoaded(component.name);
-
- // Folder A
- const folderRow = ui.measureRow(/folderA/);
- [
- [MetricKey.ncloc, '2'],
- [MetricKey.security_issues, '2'],
- [MetricKey.reliability_issues, '2'],
- [MetricKey.maintainability_issues, '2'],
- [MetricKey.security_hotspots, '2'],
- [MetricKey.coverage, '2.0%'],
- [MetricKey.duplicated_lines_density, '2.0%'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument();
- });
-
- // index.tsx
- const fileRow = ui.measureRow(/index\.tsx/);
- [
- [MetricKey.ncloc, '—'],
- [MetricKey.security_issues, '—'],
- [MetricKey.reliability_issues, '—'],
- [MetricKey.maintainability_issues, '—'],
- [MetricKey.security_hotspots, '—'],
- [MetricKey.coverage, '—'],
- [MetricKey.duplicated_lines_density, '—'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument();
- });
- });
-
- it('should correctly show new VS overall measures for Portfolios', async () => {
- const component = mockComponent({
- key: 'portfolio',
- name: 'Portfolio',
- qualifier: ComponentQualifier.Portfolio,
- canBrowseAllChildProjects: true,
- });
- componentsHandler.registerComponentTree({
- component,
- ancestors: [],
- children: [
- {
- component: mockComponent({
- analysisDate: '2022-02-01',
- key: 'child1',
- name: 'Child 1',
- }),
- ancestors: [component],
- children: [],
- },
- {
- component: mockComponent({
- key: 'child2',
- name: 'Child 2',
- }),
- ancestors: [component],
- children: [],
- },
- ],
- });
- componentsHandler.registerComponentMeasures({
- portfolio: generateMeasures('1.0', '2.0'),
- child1: generateMeasures('2.0', '3.0'),
- child2: {
- [MetricKey.alert_status]: mockMeasure({
- metric: MetricKey.alert_status,
- value: 'ERROR',
- period: undefined,
- }),
- },
- });
- const ui = getPageObject(userEvent.setup());
- renderCode({ component });
- await ui.appLoaded(component.name);
-
- // New code measures.
- expect(ui.newCodeBtn.get()).toHaveAttribute('aria-current', 'true');
-
- // Child 1
- let child1Row = ui.measureRow(/^Child 1/);
- [
- ['Releasability', 'OK'],
- ['security', 'C'],
- ['Reliability', 'C'],
- ['Maintainability', 'C'],
- ['security_review', 'C'],
- ['ncloc', '3'],
- ['last_analysis_date', '2022-02-01'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument();
- });
-
- // Child 2
- let child2Row = ui.measureRow(/^Child 2/);
- [
- ['Releasability', 'ERROR'],
- ['security', '—'],
- ['Reliability', '—'],
- ['Maintainability', '—'],
- ['security_review', '—'],
- ['ncloc', '—'],
- ['last_analysis_date', '—'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument();
- });
-
- // Overall code measures
- await ui.showOverallCode();
-
- // Child 1
- child1Row = ui.measureRow(/^Child 1/);
- [
- ['Releasability', 'OK'],
- ['security', 'B'],
- ['Reliability', 'B'],
- ['Maintainability', 'B'],
- ['security_review', 'B'],
- ['ncloc', '2'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument();
- });
-
- // Child 2
- child2Row = ui.measureRow(/^Child 2/);
- [
- ['Releasability', 'ERROR'],
- ['security', '—'],
- ['Reliability', '—'],
- ['Maintainability', '—'],
- ['security_review', '—'],
- ['ncloc', '—'],
- ].forEach(([domain, value]) => {
- expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument();
- });
- });
-
- function getPageObject(user: UserEvent) {
- const ui = {
- componentName: (name: string) => byText(name),
- childComponent: (name: string | RegExp) => byRole('cell', { name }),
- searchResult: (name: string | RegExp) => byRole('link', { name }),
- componentIsEmptyTxt: (qualifier: ComponentQualifier) =>
- byText(`code_viewer.no_source_code_displayed_due_to_empty_analysis.${qualifier}`),
- searchInput: byRole('searchbox'),
- noResultsTxt: byText('no_results'),
- sourceCode: byText('function Test() {}'),
- notAccessToAllChildrenTxt: byText('code_viewer.not_all_measures_are_shown'),
- showingOutOfTxt: (x: number, y: number) => byText(`x_of_y_shown.${x}.${y}`),
- newCodeBtn: byRole('radio', { name: 'projects.view.new_code' }),
- overallCodeBtn: byRole('radio', { name: 'projects.view.overall_code' }),
- measureRow: (name: string | RegExp) => byLabelText(name),
- measureValueCell: (row: QuerySelector, name: string, value: string) => {
- const i = Array.from(screen.getAllByRole('columnheader')).findIndex((c) =>
- c.textContent?.includes(name),
- );
- if (i < 0) {
- // eslint-disable-next-line testing-library/no-debugging-utils
- screen.debug(screen.getByRole('table'), 40000);
- throw new Error(`Couldn't locate column with header ${name}`);
- }
-
- const cell = row.byRole('cell').getAll().at(i);
-
- if (cell === undefined) {
- throw new Error(`Couldn't locate cell with value ${value} for header ${name}`);
- }
-
- return within(cell).getByText(value);
- },
- };
-
- return {
- ...ui,
- async searchForComponent(text: string) {
- await user.type(ui.searchInput.get(), text);
- },
- async clearSearch() {
- await user.clear(ui.searchInput.get());
- },
- async clickOnChildComponent(name: string | RegExp) {
- await user.click(screen.getByRole('link', { name }));
- },
- async appLoaded(name = 'Foo') {
- await waitFor(() => {
- expect(ui.componentName(name).get()).toBeInTheDocument();
- });
- },
- async clickOnBreadcrumb(name: string | RegExp) {
- await user.click(screen.getByRole('link', { name }));
- },
- async arrowDown() {
- await user.keyboard('[ArrowDown]');
- },
- async arrowRight() {
- await user.keyboard('[ArrowRight]');
- },
- async arrowLeft() {
- await user.keyboard('[ArrowLeft]');
- },
- async clickLoadMore() {
- await user.click(screen.getByRole('button', { name: 'show_more' }));
- },
- async showOverallCode() {
- await user.click(ui.overallCodeBtn.get());
- },
- };
- }
-
- function generateMeasures(overallValue = '1.0', newValue = '2.0') {
- return keyBy(
- [
- ...[
- MetricKey.security_issues,
- MetricKey.reliability_issues,
- MetricKey.maintainability_issues,
- ].map((metric) =>
- mockMeasure({ metric, value: JSON.stringify({ total: 4 }), period: undefined }),
- ),
- ...[
- MetricKey.ncloc,
- MetricKey.new_lines,
- MetricKey.bugs,
- MetricKey.new_bugs,
- MetricKey.vulnerabilities,
- MetricKey.new_vulnerabilities,
- MetricKey.code_smells,
- MetricKey.new_code_smells,
- MetricKey.security_hotspots,
- MetricKey.new_security_hotspots,
- MetricKey.coverage,
- MetricKey.new_coverage,
- MetricKey.duplicated_lines_density,
- MetricKey.new_duplicated_lines_density,
- MetricKey.releasability_rating,
- MetricKey.reliability_rating,
- MetricKey.new_reliability_rating,
- MetricKey.sqale_rating,
- MetricKey.new_maintainability_rating,
- MetricKey.security_rating,
- MetricKey.new_security_rating,
- MetricKey.security_review_rating,
- MetricKey.new_security_review_rating,
- ].map((metric) =>
- isDiffMetric(metric)
- ? mockMeasure({ metric, period: { index: 1, value: newValue } })
- : mockMeasure({ metric, value: overallValue, period: undefined }),
- ),
- mockMeasure({
- metric: MetricKey.alert_status,
- value: overallValue === '1.0' || overallValue === '2.0' ? 'OK' : 'ERROR',
- period: undefined,
- }),
- ],
- 'metric',
- );
- }
-
- function renderCode({
- component = componentsHandler.findComponentTree('foo')?.component,
- navigateTo,
- }: { component?: Component; navigateTo?: string } = {}) {
- return renderAppWithComponentContext('code', routes, { navigateTo }, { component });
- }
|